Compare commits
215 Commits
ea28436628
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
313f9fec43
|
|||
|
337ce15b37
|
|||
|
37f06fe98a
|
|||
|
79fbbd1862
|
|||
|
e8571492f0
|
|||
|
f17c88547b
|
|||
|
5439b6d370
|
|||
|
78fba57f94
|
|||
|
fa50f5c771
|
|||
|
47745bbba8
|
|||
|
e1709f8e50
|
|||
|
11388c4f35
|
|||
|
b4e32d5a6d
|
|||
|
170afb4a3b
|
|||
|
9f511c0682
|
|||
|
fec6fa7312
|
|||
|
d230d7474e
|
|||
|
545facba22
|
|||
|
550254b844
|
|||
|
cdd25236e4
|
|||
|
ee1ff5a550
|
|||
|
1a1f7ddaa9
|
|||
|
6ac2ce1197
|
|||
|
3c5e365e1a
|
|||
|
73ca60e1ce
|
|||
|
25a2bf75c5
|
|||
|
1a5deabadb
|
|||
|
17483d31fe
|
|||
|
6a890ab17f
|
|||
|
0aa39ef1f4
|
|||
|
7dc301e9f4
|
|||
|
e6fc2f6130
|
|||
|
a315eea087
|
|||
|
79ccd0036e
|
|||
|
e7df62e673
|
|||
|
83e62ba825
|
|||
|
fc29d62a00
|
|||
|
bd23a53fbb
|
|||
|
0e51c1ee39
|
|||
|
afbecff995
|
|||
|
c90c8da62e
|
|||
|
cd612ab24d
|
|||
|
eddc23a2e8
|
|||
|
c43c37a127
|
|||
|
5cf00407b4
|
|||
|
f4a5b37892
|
|||
|
3c4e078bdd
|
|||
|
6411268090
|
|||
|
0fc57ac637
|
|||
|
c9e987e2ba
|
|||
|
b2f216f1bd
|
|||
|
67e2cbbd04
|
|||
|
45159484d9
|
|||
| 7afc6ec25e | |||
| 2c22c0ec5c | |||
| 33dc448871 | |||
| 69a7756886 | |||
| f793a7516f | |||
| b7ac942807 | |||
| cee71097af | |||
| e5c12b4cfe | |||
| e3df4fcf42 | |||
| 7ad479bc87 | |||
| ff10fe10ce | |||
| 22fdcd2020 | |||
|
a1cac494dc
|
|||
|
afd37c620a
|
|||
|
f1d47a53d3
|
|||
|
8566334f59
|
|||
|
4f0b4262ed
|
|||
|
a7a6b7aa4e
|
|||
|
42fdceaf88
|
|||
|
2af9d23aba
|
|||
|
e8406f731e
|
|||
|
050504ade6
|
|||
|
6504c20708
|
|||
|
|
2ad3ba2400
|
||
|
99424ee55f
|
|||
|
9c945d69a9
|
|||
|
f5a7fa3551
|
|||
|
8f1d5280f7
|
|||
|
0ac96ab3e6
|
|||
|
a2eb882398
|
|||
|
7536fdc1ac
|
|||
|
287f315c00
|
|||
|
1504954be4
|
|||
|
82c476fa80
|
|||
|
5c6f19e8b6
|
|||
|
83cec316bc
|
|||
|
304bf0f50d
|
|||
|
6e88597af0
|
|||
|
8c90837a67
|
|||
|
c05724a9ee
|
|||
|
cbc358b96e
|
|||
|
392a15c849
|
|||
|
1d885feb1f
|
|||
|
70d1544cfe
|
|||
|
8938fa052b
|
|||
|
c9775bcd8b
|
|||
|
4715e49533
|
|||
|
e2a8abba34
|
|||
|
39f555b780
|
|||
|
2aa344a11f
|
|||
|
88a14bfced
|
|||
|
b70095c99e
|
|||
|
5da6e9ce25
|
|||
|
3b39141bf0
|
|||
|
9016b21464
|
|||
|
12a02d13dc
|
|||
|
83bd6c2830
|
|||
|
f27b991d69
|
|||
|
0f1c5b1293
|
|||
|
fabba842ce
|
|||
|
5ece89268f
|
|||
|
b8c89fcf5f
|
|||
|
65f86a8156
|
|||
|
a0f6087d3e
|
|||
|
f898243de5
|
|||
|
8d8bfa3db5
|
|||
|
220b4d2ea3
|
|||
|
2a0788ea86
|
|||
|
3ac1f4165f
|
|||
|
44a97c6d0f
|
|||
|
c75423bf84
|
|||
|
937f382f93
|
|||
|
654b196bfd
|
|||
|
f7bde8ef2e
|
|||
|
732d9866db
|
|||
|
330b037dca
|
|||
|
79dfa8499c
|
|||
|
89e7f1a41a
|
|||
|
e3c0b60337
|
|||
|
140a3070d6
|
|||
|
a56333fda8
|
|||
|
2b5f55f359
|
|||
|
e480bd6548
|
|||
|
9fb67ce2be
|
|||
|
1c7192db17
|
|||
|
9ded703143
|
|||
|
3f535a8249
|
|||
|
18fa741e4d
|
|||
|
4cd4a8cae6
|
|||
|
d90e22b641
|
|||
|
4f7632af53
|
|||
|
ca080f4e2a
|
|||
|
5a5239e335
|
|||
|
314995e5f9
|
|||
|
8e11ba4631
|
|||
|
dfd5532b20
|
|||
|
986f63c0af
|
|||
|
154c929859
|
|||
|
f779435cf0
|
|||
|
5f6eb9f2a2
|
|||
|
3f44d2d9c2
|
|||
|
b8f89ab655
|
|||
|
83df018d34
|
|||
|
7b3fe24b7c
|
|||
|
75c4edfa3d
|
|||
|
a060901cc3
|
|||
|
8e41514d05
|
|||
|
9aff7d8f26
|
|||
|
2f26b2ddb5
|
|||
|
96d76b3657
|
|||
|
4e45a9b6d0
|
|||
|
27ac4d9b4a
|
|||
|
a60a796345
|
|||
|
14f50ecdb2
|
|||
|
b1c78dce28
|
|||
|
585ec46282
|
|||
|
8f69b61799
|
|||
|
64bab332c9
|
|||
|
38401a5f69
|
|||
|
f03d472c30
|
|||
|
2d6f6700f0
|
|||
|
2e11fc5d9c
|
|||
|
ac428946e7
|
|||
|
e4329dfc2b
|
|||
|
5dbbdc62e6
|
|||
|
200614a5c9
|
|||
|
4ac5b1c101
|
|||
|
b7e6009706
|
|||
|
fd262239e4
|
|||
|
cf761d218d
|
|||
|
110627f27e
|
|||
|
64392c32c6
|
|||
|
3f8f2547be
|
|||
|
632fa6cf8e
|
|||
|
d04f8cdc44
|
|||
|
97f5677a97
|
|||
|
2ed4a4da02
|
|||
|
100fe32f8e
|
|||
|
231f591767
|
|||
|
0e7aaed154
|
|||
|
89c2d11f19
|
|||
|
cd93491d98
|
|||
|
9b83ab565a
|
|||
|
5e17bbd965
|
|||
|
de0d05df0a
|
|||
|
b2c5f8de38
|
|||
|
ecbb890cac
|
|||
|
63f8439886
|
|||
|
194f1fa1fe
|
|||
|
55afbb29b4
|
|||
|
2e76a4c6a7
|
|||
|
5c540db325
|
|||
|
4cda783fed
|
|||
|
c4951f820a
|
|||
|
a04d562d61
|
|||
|
f0cca0cda4
|
|||
|
087cd4ee51
|
|||
|
164e271d81
|
|||
|
1b2933ba0e
|
|||
|
aa85aab55e
|
|||
|
197d14fb72
|
|||
|
725fd18536
|
@@ -1 +1,2 @@
|
|||||||
TZ=Asia/Shanghai
|
TZ=Asia/Shanghai
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,3 +46,9 @@ go.work.sum
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
__MACOSX
|
__MACOSX
|
||||||
._*
|
._*
|
||||||
|
|
||||||
|
# go gen
|
||||||
|
*_gen.go
|
||||||
|
|
||||||
|
# test files
|
||||||
|
.test/
|
||||||
|
|||||||
@@ -1,26 +1,12 @@
|
|||||||
FROM docker.io/node:22-alpine AS client-build
|
|
||||||
RUN npm install -g corepack && \
|
|
||||||
corepack enable
|
|
||||||
WORKDIR /app
|
|
||||||
ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL
|
|
||||||
COPY . .
|
|
||||||
RUN cd /app/client && \
|
|
||||||
pnpm install -r --frozen-lockfile && \
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
FROM docker.io/busybox:1.37 AS client
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=client-build /app/client/dist .
|
|
||||||
EXPOSE 3000
|
|
||||||
ENTRYPOINT ["httpd", "-f", "-p", "3000", "-h", "/app", "-v"]
|
|
||||||
|
|
||||||
FROM docker.io/golang:1.25.5-alpine AS backend-build
|
FROM docker.io/golang:1.25.5-alpine AS backend-build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
RUN go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
RUN go mod tidy && \
|
RUN go mod tidy && \
|
||||||
|
go generate . && \
|
||||||
go build -o /app/nixcn-cms
|
go build -o /app/nixcn-cms
|
||||||
|
|
||||||
FROM docker.io/alpine:3.23 AS backend
|
FROM docker.io/alpine:3.23
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms
|
COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|||||||
18
api/agenda/handler.go
Normal file
18
api/agenda/handler.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package agenda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/service/service_agenda"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgendaHandler struct {
|
||||||
|
svc service_agenda.AgendaService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
agendaSvc := service_agenda.NewAgendaService()
|
||||||
|
agendaHandler := &AgendaHandler{agendaSvc}
|
||||||
|
|
||||||
|
r.POST("/submit", agendaHandler.Submit)
|
||||||
|
}
|
||||||
99
api/agenda/submit.go
Normal file
99
api/agenda/submit.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package agenda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_agenda"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Submit handles the submission of a new agenda item.
|
||||||
|
//
|
||||||
|
// @Summary Submit Agenda
|
||||||
|
// @Description Creates a new agenda item for a specific attendance record.
|
||||||
|
// @Tags Agenda
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body service_agenda.SubmitData true "Agenda Submission Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_agenda.SubmitResponse}
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /agenda/submit [post]
|
||||||
|
func (self *AgendaHandler) Submit(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(service_agenda.SubmitData)
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(data); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.EventId.String() == "" || data.Name == "" {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Submit(&service_agenda.SubmitPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), &result.Data)
|
||||||
|
}
|
||||||
87
api/auth/exchange.go
Normal file
87
api/auth/exchange.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exchange handles the authorization code swap process.
|
||||||
|
//
|
||||||
|
// @Summary Exchange Auth Code
|
||||||
|
// @Description Exchanges client credentials and user session for a specific redirect authorization code.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResponse} "Successful exchange"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/exchange [post]
|
||||||
|
func (self *AuthHandler) Exchange(c *gin.Context) {
|
||||||
|
var exchangeData service_auth.ExchangeData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&exchangeData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUnauthorized).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 401, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Exchange(&service_auth.ExchangePayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &exchangeData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
23
api/auth/handler.go
Normal file
23
api/auth/handler.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
svc service_auth.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
authSvc := service_auth.NewAuthService()
|
||||||
|
authHandler := &AuthHandler{authSvc}
|
||||||
|
|
||||||
|
r.GET("/redirect", authHandler.Redirect)
|
||||||
|
r.POST("/magic", middleware.ApiVersionCheck(), authHandler.Magic)
|
||||||
|
r.POST("/token", middleware.ApiVersionCheck(), authHandler.Token)
|
||||||
|
r.POST("/refresh", middleware.ApiVersionCheck(), authHandler.Refresh)
|
||||||
|
r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), authHandler.Exchange)
|
||||||
|
}
|
||||||
55
api/auth/magic.go
Normal file
55
api/auth/magic.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Magic handles the "Magic Link" authentication request.
|
||||||
|
//
|
||||||
|
// @Summary Request Magic Link
|
||||||
|
// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.MagicData true "Magic Link Request Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResponse} "Successful request"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/magic [post]
|
||||||
|
func (self *AuthHandler) Magic(c *gin.Context) {
|
||||||
|
var magicData service_auth.MagicData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&magicData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
magicData.ClientIP = c.ClientIP()
|
||||||
|
|
||||||
|
result := self.svc.Magic(&service_auth.MagicPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &magicData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
61
api/auth/redirect.go
Normal file
61
api/auth/redirect.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Redirect handles the post-verification callback and redirects the user to the target application.
|
||||||
|
//
|
||||||
|
// @Summary Handle Auth Callback and Redirect
|
||||||
|
// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Produce html
|
||||||
|
// @Param client_id query string true "Client Identifier"
|
||||||
|
// @Param redirect_uri query string true "Target Redirect URI"
|
||||||
|
// @Param code query string true "Temporary Verification Code"
|
||||||
|
// @Param state query string false "Opaque state used to maintain state between the request and callback"
|
||||||
|
// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Client Not Found / URI Mismatch"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Verification Code"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/redirect [get]
|
||||||
|
func (self *AuthHandler) Redirect(c *gin.Context) {
|
||||||
|
data := &service_auth.RedirectData{
|
||||||
|
ClientId: c.Query("client_id"),
|
||||||
|
RedirectUri: c.Query("redirect_uri"),
|
||||||
|
State: c.Query("state"),
|
||||||
|
Code: c.Query("code"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ClientId == "" || data.RedirectUri == "" || data.Code == "" {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Redirect(&service_auth.RedirectPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(302, result.Data)
|
||||||
|
}
|
||||||
53
api/auth/refresh.go
Normal file
53
api/auth/refresh.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refresh handles the token rotation process.
|
||||||
|
//
|
||||||
|
// @Summary Refresh Access Token
|
||||||
|
// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.RefreshData true "Refresh Token Body"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful rotation"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Invalid Refresh Token"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/refresh [post]
|
||||||
|
func (self *AuthHandler) Refresh(c *gin.Context) {
|
||||||
|
var refreshData service_auth.RefreshData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&refreshData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRefresh).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Refresh(&service_auth.RefreshPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &refreshData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
53
api/auth/token.go
Normal file
53
api/auth/token.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token exchanges an authorization code for access and refresh tokens.
|
||||||
|
//
|
||||||
|
// @Summary Exchange Code for Token
|
||||||
|
// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.TokenData true "Token Request Body"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful token issuance"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Code"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/token [post]
|
||||||
|
func (self *AuthHandler) Token(c *gin.Context) {
|
||||||
|
var tokenData service_auth.TokenData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&tokenData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceToken).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Token(&service_auth.TokenPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &tokenData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
59
api/event/attendance.go
Normal file
59
api/event/attendance.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AttendanceList handles the retrieval of the attendance list for a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Get Attendance List
|
||||||
|
// @Description Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
|
||||||
|
// @Tags Event
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]service_event.AttendanceListResponse} "Successful retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/attendance [get]
|
||||||
|
func (self *EventHandler) AttendanceList(c *gin.Context) {
|
||||||
|
eventIdStr := c.Query("event_id")
|
||||||
|
|
||||||
|
eventId, err := uuid.Parse(eventIdStr)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput). // 或者 CommonErrorUuidParseFailed
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listData := service_event.AttendanceListData{
|
||||||
|
EventId: eventId,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.AttendanceList(&service_event.AttendanceListPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &listData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
177
api/event/checkin.go
Normal file
177
api/event/checkin.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Checkin generates a check-in code for a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Generate Check-in Code
|
||||||
|
// @Description Creates a temporary check-in code for the authenticated user and event.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/checkin [get]
|
||||||
|
func (self *EventHandler) Checkin(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIdOrig := c.Query("event_id")
|
||||||
|
eventId, err := uuid.Parse(eventIdOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Checkin(&service_event.CheckinPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.CheckinData{EventId: eventId},
|
||||||
|
})
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckinSubmit validates a check-in code to complete attendance.
|
||||||
|
//
|
||||||
|
// @Summary Submit Check-in Code
|
||||||
|
// @Description Submits the generated code to mark the user as attended.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/checkin/submit [post]
|
||||||
|
func (self *EventHandler) CheckinSubmit(c *gin.Context) {
|
||||||
|
var data service_event.CheckinSubmitData
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.CheckinSubmit(&service_event.CheckinSubmitPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &data,
|
||||||
|
})
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckinQuery retrieves the check-in status of a user for an event.
|
||||||
|
//
|
||||||
|
// @Summary Query Check-in Status
|
||||||
|
// @Description Returns the timestamp of when the user checked in, or null if not yet checked in.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/checkin/query [get]
|
||||||
|
func (self *EventHandler) CheckinQuery(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIdOrig := c.Query("event_id")
|
||||||
|
eventId, err := uuid.Parse(eventIdOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.CheckinQuery(&service_event.CheckinQueryPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.CheckinQueryData{EventId: eventId},
|
||||||
|
})
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
27
api/event/handler.go
Normal file
27
api/event/handler.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandler struct {
|
||||||
|
svc service_event.EventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
eventSvc := service_event.NewEventService()
|
||||||
|
eventHandler := &EventHandler{eventSvc}
|
||||||
|
|
||||||
|
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
|
||||||
|
r.GET("/info", eventHandler.Info)
|
||||||
|
r.GET("/checkin", eventHandler.Checkin)
|
||||||
|
r.GET("/checkin/query", eventHandler.CheckinQuery)
|
||||||
|
r.POST("/checkin/submit", middleware.Permission(20), eventHandler.CheckinSubmit)
|
||||||
|
r.POST("/join", eventHandler.Join)
|
||||||
|
r.GET("/list", eventHandler.List)
|
||||||
|
r.GET("/attendance", middleware.Permission(40), eventHandler.AttendanceList)
|
||||||
|
r.GET("/joined", eventHandler.Joined)
|
||||||
|
}
|
||||||
88
api/event/info.go
Normal file
88
api/event/info.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info retrieves basic information about a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Get Event Information
|
||||||
|
// @Description Fetches the name, start time, and end time of an event using its UUID.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=data.EventIndexDoc} "Successful retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/info [get]
|
||||||
|
func (self *EventHandler) Info(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIdOrig := c.Query("event_id")
|
||||||
|
eventId, err := uuid.Parse(eventIdOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetEventInfo(&service_event.EventInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.EventInfoData{
|
||||||
|
EventId: eventId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
71
api/event/join.go
Normal file
71
api/event/join.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Join handles the request for a user to join a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Join an Event
|
||||||
|
// @Description Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body service_event.EventJoinData true "Event Join Details (UserId and EventId are required)"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_event.EventJoinResponse} "Successfully joined the event"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input or UUID Parse Failed"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized / Missing User ID / Event Limit Exceeded"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / Database Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/join [post]
|
||||||
|
func (self *EventHandler) Join(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceJoin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var joinData service_event.EventJoinData
|
||||||
|
if err := c.ShouldBindJSON(&joinData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceJoin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
joinData.UserId = userIdOrig.(string)
|
||||||
|
|
||||||
|
payload := &service_event.EventJoinPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &joinData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.JoinEvent(payload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
}
|
||||||
94
api/event/joined.go
Normal file
94
api/event/joined.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetJoined retrieves a paginated list of events that the current user has joined.
|
||||||
|
//
|
||||||
|
// @Summary Get Joined Events
|
||||||
|
// @Description Fetches a list of events where the authenticated user is a participant. Supports pagination.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Maximum number of events to return (default 20)"
|
||||||
|
// @Param offset query int false "Number of events to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.EventIndexDoc} "Successful retrieval of joined events"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/joined [get]
|
||||||
|
func (self *EventHandler) Joined(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinedQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query JoinedQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := &service_event.JoinedEventListPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.JoinedEventListData{
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetJoinedEvent(payload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
99
api/event/list.go
Normal file
99
api/event/list.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List retrieves a paginated list of events from the database.
|
||||||
|
//
|
||||||
|
// @Summary List Events
|
||||||
|
// @Description Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Maximum number of events to return (default 20)"
|
||||||
|
// @Param offset query int false "Number of events to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.EventIndexDoc} "Successful paginated list retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Missing offset or malformed parameters)"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database query failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/list [get]
|
||||||
|
func (self *EventHandler) List(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query ListQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
// Handle binding error (e.g., syntax errors in query string)
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload for the service layer
|
||||||
|
eventListPayload := &service_event.EventListPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.EventListData{
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service implementation
|
||||||
|
result := self.svc.ListEvents(eventListPayload)
|
||||||
|
|
||||||
|
// Check if the service returned any exception
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return successful response with event data
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
17
api/handler.go
Normal file
17
api/handler.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/api/auth"
|
||||||
|
"nixcn-cms/api/event"
|
||||||
|
"nixcn-cms/api/kyc"
|
||||||
|
"nixcn-cms/api/user"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(r *gin.RouterGroup) {
|
||||||
|
auth.ApiHandler(r.Group("/auth"))
|
||||||
|
user.ApiHandler(r.Group("/user"))
|
||||||
|
event.ApiHandler(r.Group("/event"))
|
||||||
|
kyc.ApiHandler(r.Group("/kyc"))
|
||||||
|
}
|
||||||
21
api/kyc/handler.go
Normal file
21
api/kyc/handler.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_kyc"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KycHandler struct {
|
||||||
|
svc service_kyc.KycService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
kycSvc := service_kyc.NewKycService()
|
||||||
|
kycHandler := &KycHandler{kycSvc}
|
||||||
|
|
||||||
|
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
|
||||||
|
r.POST("/session", kycHandler.Session)
|
||||||
|
r.POST("/query", kycHandler.Query)
|
||||||
|
}
|
||||||
66
api/kyc/query.go
Normal file
66
api/kyc/query.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_kyc"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Query KYC Status
|
||||||
|
// @Description Checks the current state of a KYC session and updates local database if approved.
|
||||||
|
// @Tags KYC
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_kyc.KycQueryData true "KYC query data (KycId)"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_kyc.KycQueryResponse} "Query processed (success/pending/failed)"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid UUID or input"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /kyc/query [post]
|
||||||
|
func (self *KycHandler) Query(c *gin.Context) {
|
||||||
|
_, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryData service_kyc.KycQueryData
|
||||||
|
if err := c.ShouldBindJSON(&queryData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPayload := &service_kyc.KycQueryPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &queryData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.QueryKyc(queryPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
68
api/kyc/session.go
Normal file
68
api/kyc/session.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_kyc"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Create KYC Session
|
||||||
|
// @Description Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.
|
||||||
|
// @Tags KYC
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_kyc.KycSessionData true "KYC session data (Type and Base64 Identity)"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_kyc.KycSessionResponse} "Session created successfully"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid input or decode failed"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / KYC Service Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /kyc/session [post]
|
||||||
|
func (self *KycHandler) Session(c *gin.Context) {
|
||||||
|
userIdFromHeaderOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceSession).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionData service_kyc.KycSessionData
|
||||||
|
if err := c.ShouldBindJSON(&sessionData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceSession).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData.UserId = userIdFromHeaderOrig.(string)
|
||||||
|
|
||||||
|
kycPayload := &service_kyc.KycSessionPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &sessionData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.SessionKyc(kycPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
@@ -2,6 +2,5 @@ package user
|
|||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
func Create(c *gin.Context) {
|
func (self *UserHandler) Create(c *gin.Context) {
|
||||||
|
|
||||||
}
|
}
|
||||||
24
api/user/handler.go
Normal file
24
api/user/handler.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserHandler struct {
|
||||||
|
svc service_user.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
userSvc := service_user.NewUserService()
|
||||||
|
userHandler := &UserHandler{userSvc}
|
||||||
|
|
||||||
|
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(5))
|
||||||
|
r.GET("/info", userHandler.Info)
|
||||||
|
r.GET("/info/:user_id", userHandler.Other)
|
||||||
|
r.PATCH("/update", userHandler.Update)
|
||||||
|
r.GET("/list", middleware.Permission(20), userHandler.List)
|
||||||
|
r.POST("/create", middleware.Permission(50), userHandler.Create)
|
||||||
|
}
|
||||||
70
api/user/info.go
Normal file
70
api/user/info.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info retrieves the profile information of the currently authenticated user.
|
||||||
|
//
|
||||||
|
// @Summary Get My User Information
|
||||||
|
// @Description Fetches the complete profile data for the user associated with the provided session/token.
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/info [get]
|
||||||
|
func (self *UserHandler) Info(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserInfoPayload := &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
IsOther: false,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetUserInfo(UserInfoPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
61
api/user/list.go
Normal file
61
api/user/list.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List retrieves a paginated list of users from the search engine.
|
||||||
|
//
|
||||||
|
// @Summary List Users
|
||||||
|
// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query string false "Maximum number of users to return (default 0)"
|
||||||
|
// @Param offset query string true "Number of users to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.UserIndexDoc} "Successful paginated list retrieval"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/list [get]
|
||||||
|
func (self *UserHandler) List(c *gin.Context) {
|
||||||
|
type ListQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query ListQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exception)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userListPayload := &service_user.UserListPayload{
|
||||||
|
Context: c,
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.ListUsers(userListPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
99
api/user/other.go
Normal file
99
api/user/other.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info retrieves the profile information of the other user.
|
||||||
|
//
|
||||||
|
// @Summary Get Other User Information
|
||||||
|
// @Description Fetches the complete profile data for the user associated with the provided session/token.
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param user_id path string true "Other user id"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "User Not Public"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/info/{user_id} [get]
|
||||||
|
func (self *UserHandler) Other(c *gin.Context) {
|
||||||
|
userIdFromUrlOrig := c.Param("user_id")
|
||||||
|
|
||||||
|
userIdFromUrl, err := uuid.Parse(userIdFromUrlOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdFromHeaderOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdFromHeader, err := uuid.Parse(userIdFromHeaderOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserInfoPayload = &service_user.UserInfoPayload{}
|
||||||
|
if userIdFromUrl == userIdFromHeader {
|
||||||
|
UserInfoPayload = &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userIdFromHeader,
|
||||||
|
IsOther: false,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
} else if userIdFromUrl != userIdFromHeader {
|
||||||
|
UserInfoPayload = &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userIdFromUrl,
|
||||||
|
IsOther: true,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetUserInfo(UserInfoPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
84
api/user/update.go
Normal file
84
api/user/update.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update modifies the profile information for the currently authenticated user.
|
||||||
|
//
|
||||||
|
// @Summary Update User Information
|
||||||
|
// @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
|
||||||
|
// @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_user.UserInfoData true "Updated User Profile Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/update [patch]
|
||||||
|
func (self *UserHandler) Update(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceUpdate).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceUpdate).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfoPayload := &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.ShouldBindJSON(&userInfoPayload.Data)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceUpdate).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.UpdateUserInfo(userInfoPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
26
client/.gitignore
vendored
26
client/.gitignore
vendored
@@ -1,26 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
.direnv
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/index.css",
|
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
|
||||||
"components": "@/components",
|
|
||||||
"utils": "@/lib/utils",
|
|
||||||
"ui": "@/components/ui",
|
|
||||||
"lib": "@/lib",
|
|
||||||
"hooks": "@/hooks"
|
|
||||||
},
|
|
||||||
"registries": {}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import antfu from '@antfu/eslint-config';
|
|
||||||
import pluginQuery from '@tanstack/eslint-plugin-query';
|
|
||||||
|
|
||||||
export default antfu({
|
|
||||||
gitignore: true,
|
|
||||||
ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**'],
|
|
||||||
react: true,
|
|
||||||
stylistic: {
|
|
||||||
semi: true,
|
|
||||||
quotes: 'single',
|
|
||||||
indent: 2,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
tsconfigPath: 'tsconfig.json',
|
|
||||||
},
|
|
||||||
}, ...pluginQuery.configs['flat/recommended']);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>client</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "client",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
|
||||||
"@marsidev/react-turnstile": "^1.4.0",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@tabler/icons-react": "^3.36.0",
|
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
|
||||||
"@tanstack/react-form": "^1.27.7",
|
|
||||||
"@tanstack/react-query": "^5.90.12",
|
|
||||||
"@tanstack/react-router": "^1.141.6",
|
|
||||||
"@tanstack/react-router-devtools": "^1.141.6",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"@tanstack/zod-adapter": "^1.143.4",
|
|
||||||
"@tanstack/zod-form-adapter": "^0.42.1",
|
|
||||||
"axios": "^1.13.2",
|
|
||||||
"base-64": "^1.0.0",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"culori": "^4.0.2",
|
|
||||||
"immer": "^11.1.0",
|
|
||||||
"lucide-react": "^0.562.0",
|
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react-hook-form": "^7.69.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"recharts": "2.15.4",
|
|
||||||
"sonner": "^2.0.7",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"tailwindcss": "^4.1.18",
|
|
||||||
"utf8": "^3.0.0",
|
|
||||||
"vaul": "^1.1.2",
|
|
||||||
"zod": "^4.2.1",
|
|
||||||
"zustand": "^5.0.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@antfu/eslint-config": "^6.7.1",
|
|
||||||
"@eslint-react/eslint-plugin": "^2.3.13",
|
|
||||||
"@eslint/js": "^9.39.1",
|
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
|
||||||
"@tanstack/router-plugin": "^1.141.7",
|
|
||||||
"@types/base-64": "^1.0.2",
|
|
||||||
"@types/culori": "^4.0.1",
|
|
||||||
"@types/node": "^25.0.3",
|
|
||||||
"@types/qrcode": "^1.5.6",
|
|
||||||
"@types/react": "^19.2.5",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@types/utf8": "^3.0.3",
|
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
|
||||||
"eslint": "^9.39.1",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
|
||||||
"globals": "^16.5.0",
|
|
||||||
"lint-staged": "^16.2.7",
|
|
||||||
"simple-git-hooks": "^2.13.1",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"typescript": "~5.9.3",
|
|
||||||
"typescript-eslint": "^8.46.4",
|
|
||||||
"vite": "^7.2.4",
|
|
||||||
"vite-plugin-svgr": "^4.5.0"
|
|
||||||
},
|
|
||||||
"simple-git-hooks": {
|
|
||||||
"pre-commit": "bun run lint-staged"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*": "eslint --fix"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7943
client/pnpm-lock.yaml
generated
7943
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
import { ThemeProvider } from '@/components/theme-provider';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
|
||||||
<p>Hello world</p>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { App };
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M120.587 309.626L148.36 261.518L269.887 472.005H214.34L186.567 423.897L158.794 472.005H131.021L117.126 447.943L158.794 375.772L120.604 309.626H120.587Z" fill="url(#paint0_linear_2199_41)"/>
|
|
||||||
<path d="M141.421 165.285H196.968L75.4412 375.772L47.6681 327.664L75.4412 279.556H19.8949L6 255.494L19.8949 231.432H103.231L141.421 165.285Z" fill="url(#paint1_linear_2199_41)"/>
|
|
||||||
<path d="M276.826 111.17L304.599 159.278H61.5632L89.3364 111.17H144.883L117.11 63.0623L131.004 39H158.778L200.446 111.17H276.826Z" fill="url(#paint2_linear_2199_41)"/>
|
|
||||||
<path d="M391.413 201.379L363.64 249.487L242.114 39H297.66L325.433 87.108L353.206 39H380.979L394.874 63.0623L353.206 135.233L391.396 201.379H391.413Z" fill="url(#paint3_linear_2199_41)"/>
|
|
||||||
<path d="M370.579 345.703H315.032L436.559 135.216L464.332 183.324L436.559 231.432H492.105L506 255.494L492.105 279.556H408.769L370.579 345.703Z" fill="url(#paint4_linear_2199_41)"/>
|
|
||||||
<path d="M235.175 399.835L207.401 351.727H450.454L422.681 399.835H367.134L394.908 447.943L381.013 472.005H353.24L311.572 399.835H235.191H235.175Z" fill="url(#paint5_linear_2199_41)"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_2199_41" x1="-244.299" y1="-121.183" x2="-163.239" y2="19.218" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_2199_41" x1="-194.029" y1="-258.424" x2="-275.089" y2="-118.023" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear_2199_41" x1="-50.0423" y1="-283.509" x2="-212.164" y2="-283.509" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint3_linear_2199_41" x1="43.6727" y1="-171.37" x2="-37.3876" y2="-311.771" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint4_linear_2199_41" x1="-6.58154" y1="-34.1292" x2="74.4793" y2="-174.531" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint5_linear_2199_41" x1="-150.568" y1="-9.02725" x2="11.5536" y2="-9.02733" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,70 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { QRCode } from '@/components/ui/shadcn-io/qr-code';
|
|
||||||
import { useCheckinCode } from '@/hooks/data/useGetCheckInCode';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
|
|
||||||
export function QrDialog(
|
|
||||||
{ eventId }: { eventId: string },
|
|
||||||
) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="w-20">签到</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>QR Code</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
请工作人员扫描下面的二维码为你签到。
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<QrSection eventId={eventId} enabled={open} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) {
|
|
||||||
const { data } = useCheckinCode(eventId, enabled);
|
|
||||||
return data
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
|
|
||||||
<QRCode data={data.data.checkin_code} className="size-60" />
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="flex flex-1 items-center ml-2 font-mono text-3xl tracking-widest text-primary/80 justify-center">
|
|
||||||
{data.data.checkin_code}
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<QrSectionSkeleton />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function QrSectionSkeleton() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
|
|
||||||
<QRCode data="114514" className="size-60 blur-xs" />
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="flex flex-1 items-center ml-2 font-mono text-3xl tracking-widest text-primary/80 justify-center">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import React, { Suspense } from 'react';
|
|
||||||
|
|
||||||
export function withFallback<P extends object>(
|
|
||||||
Component: React.ComponentType<P>,
|
|
||||||
fallback: ReactNode,
|
|
||||||
) {
|
|
||||||
const Wrapped: React.FC<P> = (props) => {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={fallback}>
|
|
||||||
<Component {...props} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Wrapped.displayName = `withFallback(${Component.displayName! || Component.name || 'Component'
|
|
||||||
})`;
|
|
||||||
|
|
||||||
return Wrapped;
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import type { TurnstileInstance } from '@marsidev/react-turnstile';
|
|
||||||
import type { AuthorizeSearchParams } from '@/routes/authorize';
|
|
||||||
import { Turnstile } from '@marsidev/react-turnstile';
|
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import NixOSLogo from '@/assets/nixos.svg?react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { useGetMagicLink } from '@/hooks/data/useGetMagicLink';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
export function LoginForm({
|
|
||||||
oauthParams,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
oauthParams: AuthorizeSearchParams;
|
|
||||||
}) {
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
|
||||||
const turnstileRef = useRef<TurnstileInstance>(null);
|
|
||||||
const [token, setToken] = useState<string | null>(null);
|
|
||||||
const { mutateAsync, isPending } = useGetMagicLink();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const formData = new FormData(formRef.current!);
|
|
||||||
const email = formData.get('email')! as string;
|
|
||||||
mutateAsync({ email, turnstile_token: token!, ...oauthParams }).then(() => {
|
|
||||||
void navigate({ to: '/magicLinkSent', search: { email } });
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error('请求登录链接失败');
|
|
||||||
turnstileRef.current?.reset();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
||||||
<form ref={formRef} onSubmit={handleSubmit}>
|
|
||||||
<FieldGroup>
|
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
|
||||||
<div className="flex size-8 items-center justify-center rounded-md">
|
|
||||||
<NixOSLogo className="size-6" />
|
|
||||||
</div>
|
|
||||||
<span className="sr-only">Nix CN Meetup #2</span>
|
|
||||||
<h1 className="text-xl font-bold">欢迎来到 Nix CN Meetup #2</h1>
|
|
||||||
</div>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="email">参会登记Email</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="edolstra@gmail.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Button type="submit" disabled={token === null || isPending}>
|
|
||||||
{token === null ? '等待 Turnstile' : isPending ? '发送中...' : '发送登录链接'}
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
<Turnstile
|
|
||||||
ref={turnstileRef}
|
|
||||||
siteKey="0x4AAAAAACI5pu-lNWFc6Wu1"
|
|
||||||
options={{
|
|
||||||
refreshExpired: 'auto',
|
|
||||||
}}
|
|
||||||
onSuccess={(token) => {
|
|
||||||
setToken(token);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import z from 'zod';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldError,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field';
|
|
||||||
import {
|
|
||||||
Input,
|
|
||||||
} from '@/components/ui/input';
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
email: z.string(),
|
|
||||||
nickname: z.string().min(1),
|
|
||||||
subtitle: z.string().min(1),
|
|
||||||
});
|
|
||||||
export function EditProfileDialog() {
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
nickname: '',
|
|
||||||
subtitle: '',
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onBlur: formSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async ({
|
|
||||||
value,
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
toast(
|
|
||||||
<code className="text-white">{JSON.stringify(value, null, 2)}</code>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Form submission error', error);
|
|
||||||
toast.error('Failed to submit the form. Please try again.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" className="w-full mt-4" size="lg">编辑个人资料</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void form.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>编辑个人资料</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
placeholder="noa@requiem.garden"
|
|
||||||
value={form.getFieldValue('email')}
|
|
||||||
onChange={e => form.setFieldValue('email', e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="nickname">昵称</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="nickname"
|
|
||||||
name="nickname"
|
|
||||||
placeholder="Noa Virellia"
|
|
||||||
value={form.getFieldValue('nickname')}
|
|
||||||
onChange={e => form.setFieldValue('nickname', e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="subtitle">副标题</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="subtitle"
|
|
||||||
name="subtitle"
|
|
||||||
placeholder="天生骄傲"
|
|
||||||
value={form.getFieldValue('subtitle')}
|
|
||||||
onChange={e => form.setFieldValue('subtitle', e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError />
|
|
||||||
</Field>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">取消</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit">保存</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import {
|
|
||||||
useForm,
|
|
||||||
} from '@tanstack/react-form';
|
|
||||||
import {
|
|
||||||
toast,
|
|
||||||
} from 'sonner';
|
|
||||||
import {
|
|
||||||
z,
|
|
||||||
} from 'zod';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
} from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldError,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field';
|
|
||||||
import {
|
|
||||||
Input,
|
|
||||||
} from '@/components/ui/input';
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
email: z.string(),
|
|
||||||
nickname: z.string().min(1),
|
|
||||||
subtitle: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function EditProfileForm() {
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
nickname: '',
|
|
||||||
subtitle: '',
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onBlur: formSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async ({
|
|
||||||
value,
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
toast(
|
|
||||||
<code className="text-white">{JSON.stringify(value, null, 2)}</code>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Form submission error', error);
|
|
||||||
toast.error('Failed to submit the form. Please try again.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void form.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
placeholder="noa@requiem.garden"
|
|
||||||
|
|
||||||
value={form.getFieldValue('email')}
|
|
||||||
onChange={e => form.setFieldValue('email', e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="nickname">昵称</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="nickname"
|
|
||||||
name="nickname"
|
|
||||||
placeholder="Noa Virellia"
|
|
||||||
|
|
||||||
value={form.getFieldValue('nickname')}
|
|
||||||
onChange={e => form.setFieldValue('nickname', e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldError />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="subtitle">副标题</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="subtitle"
|
|
||||||
name="subtitle"
|
|
||||||
placeholder="天生骄傲"
|
|
||||||
|
|
||||||
value={form.getFieldValue('subtitle')}
|
|
||||||
onChange={e => form.setFieldValue('subtitle', e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldError />
|
|
||||||
</Field>
|
|
||||||
<Button type="submit">提交</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Mail } from 'lucide-react';
|
|
||||||
import Markdown from 'react-markdown';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { base64ToUtf8 } from '@/lib/utils';
|
|
||||||
import { EditProfileDialog } from './edit-profile-dialog';
|
|
||||||
|
|
||||||
export function MainProfile() {
|
|
||||||
const { data: user } = useUserInfo();
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="flex w-full flex-row gap-4 mt-2">
|
|
||||||
<Avatar className="size-16 rounded-full border-2 border-muted">
|
|
||||||
<AvatarImage src={user.avatar} alt={user.nickname} />
|
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-1 flex-col justify-center">
|
|
||||||
<span className="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span>
|
|
||||||
<span className="text-[20px] text-muted-foreground" aria-hidden="true">{user.subtitle}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<EditProfileDialog />
|
|
||||||
<section className="px-2 mt-4">
|
|
||||||
<div className="flex flex-row gap-2 items-center text-sm">
|
|
||||||
<Mail className="h-4 w-4 stroke-muted-foreground" />
|
|
||||||
{user.email}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="rounded-md border border-muted w-full min-h-72 mt-4 p-6 prose dark:prose-invert max-w-[1012px] self-center">
|
|
||||||
{/* Bio */}
|
|
||||||
<Markdown>{base64ToUtf8(user.bio)}</Markdown>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import NixOSLogo from '@/assets/nixos.svg?react';
|
|
||||||
import { NavMain } from '@/components/sidebar/nav-main';
|
|
||||||
import { NavSecondary } from '@/components/sidebar/nav-secondary';
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
import { navData } from '@/lib/navData';
|
|
||||||
import { NavUser } from './nav-user';
|
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
||||||
return (
|
|
||||||
<Sidebar collapsible="offcanvas" {...props}>
|
|
||||||
<SidebarHeader>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
|
||||||
>
|
|
||||||
<a href="#">
|
|
||||||
<NixOSLogo />
|
|
||||||
<span className="text-base font-semibold">Nix CN CMS</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent>
|
|
||||||
<NavMain items={navData.navMain} />
|
|
||||||
<NavSecondary items={navData.navSecondary} className="mt-auto" />
|
|
||||||
</SidebarContent>
|
|
||||||
<SidebarFooter>
|
|
||||||
<NavUser />
|
|
||||||
</SidebarFooter>
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import type { Icon } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
import { Link } from '@tanstack/react-router';
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export function NavMain({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
icon?: Icon;
|
|
||||||
}[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupContent className="flex flex-col gap-2">
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map(item => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<Link
|
|
||||||
to={item.url}
|
|
||||||
>
|
|
||||||
{({ isActive }) => {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton isActive={isActive} tooltip={item.title}>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { Icon } from '@tabler/icons-react';
|
|
||||||
import { Link } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export function NavSecondary({
|
|
||||||
items,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
icon: Icon;
|
|
||||||
}[];
|
|
||||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup {...props}>
|
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map(item => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<Link to={item.url}>
|
|
||||||
{({ isActive }) => {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton isActive={isActive} tooltip={item.title}>
|
|
||||||
<item.icon />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import {
|
|
||||||
IconDotsVertical,
|
|
||||||
IconLogout,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from '@/components/ui/avatar';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import {
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { useLogout } from '@/hooks/useLogout';
|
|
||||||
import { withFallback } from '../hoc/with-fallback';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
function NavUser_() {
|
|
||||||
const { isMobile } = useSidebar();
|
|
||||||
const { data: user } = useUserInfo();
|
|
||||||
const { logout } = useLogout();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<SidebarMenuButton
|
|
||||||
size="lg"
|
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
||||||
>
|
|
||||||
<Avatar className="h-8 w-8 rounded-lg grayscale">
|
|
||||||
<AvatarImage src={user.avatar} alt={user.nickname} />
|
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-medium">{user.nickname}</span>
|
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<IconDotsVertical className="ml-auto size-4" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
||||||
side={isMobile ? 'bottom' : 'right'}
|
|
||||||
align="end"
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
|
||||||
<AvatarImage src={user.avatar} alt={user.nickname} />
|
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-medium">{user.nickname}</span>
|
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={logout}>
|
|
||||||
<IconLogout />
|
|
||||||
登出
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavUserSkeleton() {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
|
||||||
<div className="flex flex-col flex-1 gap-1">
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
<Skeleton className="h-3 w-24" />
|
|
||||||
</div>
|
|
||||||
<IconDotsVertical className="ml-auto size-4" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NavUser = withFallback(NavUser_, <NavUserSkeleton />);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useRouterState } from '@tanstack/react-router';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
|
||||||
import { navData } from '@/lib/navData';
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
|
||||||
const pathname = useRouterState({ select: state => state.location.pathname });
|
|
||||||
const allNavItems = [...navData.navMain, ...navData.navSecondary];
|
|
||||||
const currentTitle
|
|
||||||
= allNavItems.find(item =>
|
|
||||||
item.url === '/'
|
|
||||||
? pathname === '/'
|
|
||||||
: pathname.startsWith(item.url),
|
|
||||||
)?.title ?? '工作台';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
|
||||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
||||||
<SidebarTrigger className="-ml-1" />
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="mx-2 data-[orientation=vertical]:h-4"
|
|
||||||
/>
|
|
||||||
<h1 className="text-base font-medium">{currentTitle}</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import type { Theme } from '@/hooks/useTheme';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ThemeProviderContext } from '@/hooks/useTheme';
|
|
||||||
|
|
||||||
interface ThemeProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
defaultTheme?: Theme;
|
|
||||||
storageKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThemeProvider({
|
|
||||||
children,
|
|
||||||
defaultTheme = 'dark',
|
|
||||||
storageKey = 'vite-ui-theme',
|
|
||||||
...props
|
|
||||||
}: ThemeProviderProps) {
|
|
||||||
const [theme, setTheme] = useState<Theme>(
|
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
|
|
||||||
root.classList.remove('light', 'dark');
|
|
||||||
|
|
||||||
if (theme === 'system') {
|
|
||||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
.matches
|
|
||||||
? 'dark'
|
|
||||||
: 'light';
|
|
||||||
|
|
||||||
root.classList.add(systemTheme);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.classList.add(theme);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/no-unstable-context-value
|
|
||||||
const value = {
|
|
||||||
theme,
|
|
||||||
setTheme: (theme: Theme) => {
|
|
||||||
localStorage.setItem(storageKey, theme);
|
|
||||||
setTheme(theme);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProviderContext {...props} value={value}>
|
|
||||||
{children}
|
|
||||||
</ThemeProviderContext>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Avatar({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
data-slot="avatar"
|
|
||||||
className={cn(
|
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarImage({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
data-slot="avatar-image"
|
|
||||||
className={cn("aspect-square size-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarFallback({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
data-slot="avatar-fallback"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Badge({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span"> &
|
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "span"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="badge"
|
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
||||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
||||||
return (
|
|
||||||
<ol
|
|
||||||
data-slot="breadcrumb-list"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="breadcrumb-item"
|
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbLink({
|
|
||||||
asChild,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="breadcrumb-link"
|
|
||||||
className={cn("hover:text-foreground transition-colors", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="breadcrumb-page"
|
|
||||||
role="link"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-current="page"
|
|
||||||
className={cn("text-foreground font-normal", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="breadcrumb-separator"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("[&>svg]:size-3.5", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <ChevronRight />}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="breadcrumb-ellipsis"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="size-4" />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
BreadcrumbEllipsis,
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import type { VariantProps } from 'class-variance-authority';
|
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
|
||||||
import { cva } from 'class-variance-authority';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
||||||
destructive:
|
|
||||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
||||||
outline:
|
|
||||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
||||||
secondary:
|
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
||||||
ghost:
|
|
||||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
'default': 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
||||||
'sm': 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
||||||
'lg': 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
||||||
'icon': 'size-9',
|
|
||||||
'icon-sm': 'size-8',
|
|
||||||
'icon-lg': 'size-10',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: 'default',
|
|
||||||
size: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'default',
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'button'>
|
|
||||||
& VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : 'button';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
className={cn(
|
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn("leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-action"
|
|
||||||
className={cn(
|
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-content"
|
|
||||||
className={cn("px-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as RechartsPrimitive from "recharts"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
|
||||||
|
|
||||||
export type ChartConfig = {
|
|
||||||
[k in string]: {
|
|
||||||
label?: React.ReactNode
|
|
||||||
icon?: React.ComponentType
|
|
||||||
} & (
|
|
||||||
| { color?: string; theme?: never }
|
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartContextProps = {
|
|
||||||
config: ChartConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
||||||
|
|
||||||
function useChart() {
|
|
||||||
const context = React.useContext(ChartContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChartContainer({
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
config,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
config: ChartConfig
|
|
||||||
children: React.ComponentProps<
|
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
|
||||||
>["children"]
|
|
||||||
}) {
|
|
||||||
const uniqueId = React.useId()
|
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChartContext.Provider value={{ config }}>
|
|
||||||
<div
|
|
||||||
data-slot="chart"
|
|
||||||
data-chart={chartId}
|
|
||||||
className={cn(
|
|
||||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChartStyle id={chartId} config={config} />
|
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</ChartContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
||||||
const colorConfig = Object.entries(config).filter(
|
|
||||||
([, config]) => config.theme || config.color
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: Object.entries(THEMES)
|
|
||||||
.map(
|
|
||||||
([theme, prefix]) => `
|
|
||||||
${prefix} [data-chart=${id}] {
|
|
||||||
${colorConfig
|
|
||||||
.map(([key, itemConfig]) => {
|
|
||||||
const color =
|
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
||||||
itemConfig.color
|
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
|
||||||
})
|
|
||||||
.join("\n")}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
||||||
|
|
||||||
function ChartTooltipContent({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
className,
|
|
||||||
indicator = "dot",
|
|
||||||
hideLabel = false,
|
|
||||||
hideIndicator = false,
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
labelClassName,
|
|
||||||
formatter,
|
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean
|
|
||||||
hideIndicator?: boolean
|
|
||||||
indicator?: "line" | "dot" | "dashed"
|
|
||||||
nameKey?: string
|
|
||||||
labelKey?: string
|
|
||||||
}) {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
|
||||||
if (hideLabel || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const [item] = payload
|
|
||||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const value =
|
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
|
||||||
return (
|
|
||||||
<div className={cn("font-medium", labelClassName)}>
|
|
||||||
{labelFormatter(value, payload)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
||||||
}, [
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
payload,
|
|
||||||
hideLabel,
|
|
||||||
labelClassName,
|
|
||||||
config,
|
|
||||||
labelKey,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload
|
|
||||||
.filter((item) => item.type !== "none")
|
|
||||||
.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.dataKey}
|
|
||||||
className={cn(
|
|
||||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
|
||||||
indicator === "dot" && "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
||||||
{
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.value && (
|
|
||||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
|
||||||
|
|
||||||
function ChartLegendContent({
|
|
||||||
className,
|
|
||||||
hideIcon = false,
|
|
||||||
payload,
|
|
||||||
verticalAlign = "bottom",
|
|
||||||
nameKey,
|
|
||||||
}: React.ComponentProps<"div"> &
|
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
||||||
hideIcon?: boolean
|
|
||||||
nameKey?: string
|
|
||||||
}) {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
if (!payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{payload
|
|
||||||
.filter((item) => item.type !== "none")
|
|
||||||
.map((item) => {
|
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
className={cn(
|
|
||||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
||||||
style={{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{itemConfig?.label}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
|
||||||
function getPayloadConfigFromPayload(
|
|
||||||
config: ChartConfig,
|
|
||||||
payload: unknown,
|
|
||||||
key: string
|
|
||||||
) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadPayload =
|
|
||||||
"payload" in payload &&
|
|
||||||
typeof payload.payload === "object" &&
|
|
||||||
payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let configLabelKey: string = key
|
|
||||||
|
|
||||||
if (
|
|
||||||
key in payload &&
|
|
||||||
typeof payload[key as keyof typeof payload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
|
||||||
} else if (
|
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[
|
|
||||||
key as keyof typeof payloadPayload
|
|
||||||
] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config
|
|
||||||
? config[configLabelKey]
|
|
||||||
: config[key as keyof typeof config]
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Checkbox({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
data-slot="checkbox"
|
|
||||||
className={cn(
|
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
data-slot="checkbox-indicator"
|
|
||||||
className="grid place-content-center text-current transition-none"
|
|
||||||
>
|
|
||||||
<CheckIcon className="size-3.5" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Dialog({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
data-slot="dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showCloseButton = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DialogPortal data-slot="dialog-portal">
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
data-slot="dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
data-slot="dialog-close"
|
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
data-slot="dialog-title"
|
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
data-slot="dialog-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Drawer as DrawerPrimitive } from "vaul"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Drawer({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
|
||||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
|
||||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
|
||||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
|
||||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Overlay
|
|
||||||
data-slot="drawer-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DrawerPortal data-slot="drawer-portal">
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerPrimitive.Content
|
|
||||||
data-slot="drawer-content"
|
|
||||||
className={cn(
|
|
||||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
|
||||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
|
||||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
|
||||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
|
||||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
|
||||||
{children}
|
|
||||||
</DrawerPrimitive.Content>
|
|
||||||
</DrawerPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="drawer-header"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="drawer-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Title
|
|
||||||
data-slot="drawer-title"
|
|
||||||
className={cn("text-foreground font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Description
|
|
||||||
data-slot="drawer-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Drawer,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerOverlay,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerDescription,
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function DropdownMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Trigger
|
|
||||||
data-slot="dropdown-menu-trigger"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import type { VariantProps } from 'class-variance-authority';
|
|
||||||
import { cva } from 'class-variance-authority';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
|
||||||
return (
|
|
||||||
<fieldset
|
|
||||||
data-slot="field-set"
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col gap-6',
|
|
||||||
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLegend({
|
|
||||||
className,
|
|
||||||
variant = 'legend',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
|
|
||||||
return (
|
|
||||||
<legend
|
|
||||||
data-slot="field-legend"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
'mb-3 font-medium',
|
|
||||||
'data-[variant=legend]:text-base',
|
|
||||||
'data-[variant=label]:text-sm',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className={cn(
|
|
||||||
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
|
||||||
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
|
|
||||||
horizontal: [
|
|
||||||
'flex-row items-center',
|
|
||||||
'[&>[data-slot=field-label]]:flex-auto',
|
|
||||||
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
||||||
],
|
|
||||||
responsive: [
|
|
||||||
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
|
|
||||||
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
|
|
||||||
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: 'vertical',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function Field({
|
|
||||||
className,
|
|
||||||
orientation = 'vertical',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="field"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-content"
|
|
||||||
className={cn(
|
|
||||||
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
|
||||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
|
|
||||||
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
|
||||||
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
|
||||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-separator"
|
|
||||||
data-content={!!children}
|
|
||||||
className={cn(
|
|
||||||
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
|
||||||
{children && (
|
|
||||||
<span
|
|
||||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
|
||||||
data-slot="field-separator-content"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldError({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
errors,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
errors?: Array<{ message?: string } | undefined>;
|
|
||||||
}) {
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (children) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueErrors = [
|
|
||||||
...new Map(errors.map(error => [error?.message, error])).values(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (uniqueErrors?.length == 1) {
|
|
||||||
return uniqueErrors[0]?.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
||||||
{uniqueErrors.map(
|
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>,
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}, [children, errors]);
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
data-slot="field-error"
|
|
||||||
className={cn('text-destructive text-sm font-normal', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input };
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label };
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Select({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
||||||
size?: "sm" | "default"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
data-slot="select-trigger"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
position = "item-aligned",
|
|
||||||
align = "center",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
data-slot="select-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
align={align}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
data-slot="select-label"
|
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
data-slot="select-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-slot="select-item-indicator"
|
|
||||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
|
||||||
>
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
data-slot="select-separator"
|
|
||||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
data-slot="select-scroll-up-button"
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
data-slot="select-scroll-down-button"
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Separator({
|
|
||||||
className,
|
|
||||||
orientation = 'horizontal',
|
|
||||||
decorative = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SeparatorPrimitive.Root
|
|
||||||
data-slot="separator"
|
|
||||||
decorative={decorative}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Separator };
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { formatHex, oklch } from 'culori';
|
|
||||||
import QR from 'qrcode';
|
|
||||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type QRCodeProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
data: string;
|
|
||||||
foreground?: string;
|
|
||||||
background?: string;
|
|
||||||
robustness?: 'L' | 'M' | 'Q' | 'H';
|
|
||||||
};
|
|
||||||
|
|
||||||
const oklchRegex = /oklch\(([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\)/;
|
|
||||||
|
|
||||||
const getOklch = (color: string, fallback: [number, number, number]) => {
|
|
||||||
const oklchMatch = color.match(oklchRegex);
|
|
||||||
|
|
||||||
if (!oklchMatch) {
|
|
||||||
return { l: fallback[0], c: fallback[1], h: fallback[2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
l: Number.parseFloat(oklchMatch[1]),
|
|
||||||
c: Number.parseFloat(oklchMatch[2]),
|
|
||||||
h: Number.parseFloat(oklchMatch[3]),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QRCode = ({
|
|
||||||
data,
|
|
||||||
foreground,
|
|
||||||
background,
|
|
||||||
robustness = 'M',
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QRCodeProps) => {
|
|
||||||
const [svg, setSVG] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const generateQR = async () => {
|
|
||||||
try {
|
|
||||||
const styles = getComputedStyle(document.documentElement);
|
|
||||||
const foregroundColor =
|
|
||||||
foreground ?? styles.getPropertyValue('--foreground');
|
|
||||||
const backgroundColor =
|
|
||||||
background ?? styles.getPropertyValue('--background');
|
|
||||||
|
|
||||||
const foregroundOklch = getOklch(
|
|
||||||
foregroundColor,
|
|
||||||
[0.21, 0.006, 285.885]
|
|
||||||
);
|
|
||||||
const backgroundOklch = getOklch(backgroundColor, [0.985, 0, 0]);
|
|
||||||
|
|
||||||
const newSvg = await QR.toString(data, {
|
|
||||||
type: 'svg',
|
|
||||||
color: {
|
|
||||||
dark: formatHex(oklch({ mode: 'oklch', ...foregroundOklch })),
|
|
||||||
light: formatHex(oklch({ mode: 'oklch', ...backgroundOklch })),
|
|
||||||
},
|
|
||||||
width: 200,
|
|
||||||
errorCorrectionLevel: robustness,
|
|
||||||
margin: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSVG(newSvg);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateQR();
|
|
||||||
}, [data, foreground, background, robustness]);
|
|
||||||
|
|
||||||
if (!svg) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn('size-full', '[&_svg]:size-full', className)}
|
|
||||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Required for SVG"
|
|
||||||
dangerouslySetInnerHTML={{ __html: svg }}
|
|
||||||
{...(props as any)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Overlay
|
|
||||||
data-slot="sheet-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
side = "right",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content
|
|
||||||
data-slot="sheet-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
||||||
side === "right" &&
|
|
||||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
||||||
side === "left" &&
|
|
||||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
||||||
side === "top" &&
|
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
|
||||||
side === "bottom" &&
|
|
||||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
|
||||||
<XIcon className="size-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-header"
|
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
data-slot="sheet-title"
|
|
||||||
className={cn("text-foreground font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
data-slot="sheet-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
}
|
|
||||||
@@ -1,726 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
} from "@/components/ui/sheet"
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip"
|
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
|
||||||
|
|
||||||
type SidebarContextProps = {
|
|
||||||
state: "expanded" | "collapsed"
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
openMobile: boolean
|
|
||||||
setOpenMobile: (open: boolean) => void
|
|
||||||
isMobile: boolean
|
|
||||||
toggleSidebar: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
|
||||||
|
|
||||||
function useSidebar() {
|
|
||||||
const context = React.useContext(SidebarContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarProvider({
|
|
||||||
defaultOpen = true,
|
|
||||||
open: openProp,
|
|
||||||
onOpenChange: setOpenProp,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
defaultOpen?: boolean
|
|
||||||
open?: boolean
|
|
||||||
onOpenChange?: (open: boolean) => void
|
|
||||||
}) {
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const [openMobile, setOpenMobile] = React.useState(false)
|
|
||||||
|
|
||||||
// This is the internal state of the sidebar.
|
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
|
||||||
const open = openProp ?? _open
|
|
||||||
const setOpen = React.useCallback(
|
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
|
||||||
const openState = typeof value === "function" ? value(open) : value
|
|
||||||
if (setOpenProp) {
|
|
||||||
setOpenProp(openState)
|
|
||||||
} else {
|
|
||||||
_setOpen(openState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
||||||
},
|
|
||||||
[setOpenProp, open]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
|
||||||
const toggleSidebar = React.useCallback(() => {
|
|
||||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
||||||
(event.metaKey || event.ctrlKey)
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleSidebar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}, [toggleSidebar])
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
|
||||||
const state = open ? "expanded" : "collapsed"
|
|
||||||
|
|
||||||
const contextValue = React.useMemo<SidebarContextProps>(
|
|
||||||
() => ({
|
|
||||||
state,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
isMobile,
|
|
||||||
openMobile,
|
|
||||||
setOpenMobile,
|
|
||||||
toggleSidebar,
|
|
||||||
}),
|
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarContext.Provider value={contextValue}>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-wrapper"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH,
|
|
||||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
||||||
...style,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</SidebarContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Sidebar({
|
|
||||||
side = "left",
|
|
||||||
variant = "sidebar",
|
|
||||||
collapsible = "offcanvas",
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
side?: "left" | "right"
|
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
|
||||||
}) {
|
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
|
||||||
|
|
||||||
if (collapsible === "none") {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar"
|
|
||||||
className={cn(
|
|
||||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
|
||||||
<SheetContent
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-slot="sidebar"
|
|
||||||
data-mobile="true"
|
|
||||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
side={side}
|
|
||||||
>
|
|
||||||
<SheetHeader className="sr-only">
|
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="group peer text-sidebar-foreground hidden md:block"
|
|
||||||
data-state={state}
|
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
|
||||||
data-variant={variant}
|
|
||||||
data-side={side}
|
|
||||||
data-slot="sidebar"
|
|
||||||
>
|
|
||||||
{/* This is what handles the sidebar gap on desktop */}
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-gap"
|
|
||||||
className={cn(
|
|
||||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
|
||||||
"group-data-[collapsible=offcanvas]:w-0",
|
|
||||||
"group-data-[side=right]:rotate-180",
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-container"
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
|
||||||
side === "left"
|
|
||||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
|
||||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
|
||||||
// Adjust the padding for floating and inset variants.
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-slot="sidebar-inner"
|
|
||||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarTrigger({
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Button>) {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-sidebar="trigger"
|
|
||||||
data-slot="sidebar-trigger"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn("size-7", className)}
|
|
||||||
onClick={(event) => {
|
|
||||||
onClick?.(event)
|
|
||||||
toggleSidebar()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<PanelLeftIcon />
|
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
data-sidebar="rail"
|
|
||||||
data-slot="sidebar-rail"
|
|
||||||
aria-label="Toggle Sidebar"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
title="Toggle Sidebar"
|
|
||||||
className={cn(
|
|
||||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
|
||||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
|
||||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
|
||||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
|
||||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
data-slot="sidebar-inset"
|
|
||||||
className={cn(
|
|
||||||
"bg-background relative flex w-full flex-1 flex-col",
|
|
||||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Input>) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="sidebar-input"
|
|
||||||
data-sidebar="input"
|
|
||||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-header"
|
|
||||||
data-sidebar="header"
|
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-footer"
|
|
||||||
data-sidebar="footer"
|
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
data-slot="sidebar-separator"
|
|
||||||
data-sidebar="separator"
|
|
||||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-content"
|
|
||||||
data-sidebar="content"
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-group"
|
|
||||||
data-sidebar="group"
|
|
||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupLabel({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "div"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-group-label"
|
|
||||||
data-sidebar="group-label"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupAction({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-group-action"
|
|
||||||
data-sidebar="group-action"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
// Increases the hit area of the button on mobile.
|
|
||||||
"after:absolute after:-inset-2 md:after:hidden",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-group-content"
|
|
||||||
data-sidebar="group-content"
|
|
||||||
className={cn("w-full text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
data-slot="sidebar-menu"
|
|
||||||
data-sidebar="menu"
|
|
||||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="sidebar-menu-item"
|
|
||||||
data-sidebar="menu-item"
|
|
||||||
className={cn("group/menu-item relative", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
||||||
outline:
|
|
||||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-8 text-sm",
|
|
||||||
sm: "h-7 text-xs",
|
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function SidebarMenuButton({
|
|
||||||
asChild = false,
|
|
||||||
isActive = false,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
tooltip,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
isActive?: boolean
|
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
const { isMobile, state } = useSidebar()
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-menu-button"
|
|
||||||
data-sidebar="menu-button"
|
|
||||||
data-size={size}
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!tooltip) {
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof tooltip === "string") {
|
|
||||||
tooltip = {
|
|
||||||
children: tooltip,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="right"
|
|
||||||
align="center"
|
|
||||||
hidden={state !== "collapsed" || isMobile}
|
|
||||||
{...tooltip}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuAction({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
showOnHover = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
showOnHover?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-menu-action"
|
|
||||||
data-sidebar="menu-action"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
// Increases the hit area of the button on mobile.
|
|
||||||
"after:absolute after:-inset-2 md:after:hidden",
|
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
showOnHover &&
|
|
||||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuBadge({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-menu-badge"
|
|
||||||
data-sidebar="menu-badge"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
|
||||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSkeleton({
|
|
||||||
className,
|
|
||||||
showIcon = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
showIcon?: boolean
|
|
||||||
}) {
|
|
||||||
// Random width between 50 to 90%.
|
|
||||||
const width = React.useMemo(() => {
|
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-menu-skeleton"
|
|
||||||
data-sidebar="menu-skeleton"
|
|
||||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{showIcon && (
|
|
||||||
<Skeleton
|
|
||||||
className="size-4 rounded-md"
|
|
||||||
data-sidebar="menu-skeleton-icon"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Skeleton
|
|
||||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
|
||||||
data-sidebar="menu-skeleton-text"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--skeleton-width": width,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
data-slot="sidebar-menu-sub"
|
|
||||||
data-sidebar="menu-sub"
|
|
||||||
className={cn(
|
|
||||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSubItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="sidebar-menu-sub-item"
|
|
||||||
data-sidebar="menu-sub-item"
|
|
||||||
className={cn("group/menu-sub-item relative", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSubButton({
|
|
||||||
asChild = false,
|
|
||||||
size = "md",
|
|
||||||
isActive = false,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
size?: "sm" | "md"
|
|
||||||
isActive?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-menu-sub-button"
|
|
||||||
data-sidebar="menu-sub-button"
|
|
||||||
data-size={size}
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
|
||||||
size === "sm" && "text-xs",
|
|
||||||
size === "md" && "text-sm",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupAction,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInput,
|
|
||||||
SidebarInset,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger,
|
|
||||||
useSidebar,
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton }
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {
|
|
||||||
CircleCheckIcon,
|
|
||||||
InfoIcon,
|
|
||||||
Loader2Icon,
|
|
||||||
OctagonXIcon,
|
|
||||||
TriangleAlertIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
|
||||||
const { theme = "system" } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sonner
|
|
||||||
theme={theme as ToasterProps["theme"]}
|
|
||||||
className="toaster group"
|
|
||||||
icons={{
|
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
|
||||||
info: <InfoIcon className="size-4" />,
|
|
||||||
warning: <TriangleAlertIcon className="size-4" />,
|
|
||||||
error: <OctagonXIcon className="size-4" />,
|
|
||||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
||||||
}}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--normal-bg": "var(--popover)",
|
|
||||||
"--normal-text": "var(--popover-foreground)",
|
|
||||||
"--normal-border": "var(--border)",
|
|
||||||
"--border-radius": "var(--radius)",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Toaster }
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="table-container"
|
|
||||||
className="relative w-full overflow-x-auto"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
data-slot="table"
|
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
||||||
return (
|
|
||||||
<thead
|
|
||||||
data-slot="table-header"
|
|
||||||
className={cn("[&_tr]:border-b", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
||||||
return (
|
|
||||||
<tbody
|
|
||||||
data-slot="table-body"
|
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
||||||
return (
|
|
||||||
<tfoot
|
|
||||||
data-slot="table-footer"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
data-slot="table-row"
|
|
||||||
className={cn(
|
|
||||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
data-slot="table-head"
|
|
||||||
className={cn(
|
|
||||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
data-slot="table-cell"
|
|
||||||
className={cn(
|
|
||||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableCaption({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"caption">) {
|
|
||||||
return (
|
|
||||||
<caption
|
|
||||||
data-slot="table-caption"
|
|
||||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Tabs({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
data-slot="tabs-list"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
data-slot="tabs-trigger"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
|
||||||
import { type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { toggleVariants } from "@/components/ui/toggle"
|
|
||||||
|
|
||||||
const ToggleGroupContext = React.createContext<
|
|
||||||
VariantProps<typeof toggleVariants> & {
|
|
||||||
spacing?: number
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
size: "default",
|
|
||||||
variant: "default",
|
|
||||||
spacing: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
function ToggleGroup({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
spacing = 0,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
|
||||||
VariantProps<typeof toggleVariants> & {
|
|
||||||
spacing?: number
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ToggleGroupPrimitive.Root
|
|
||||||
data-slot="toggle-group"
|
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
data-spacing={spacing}
|
|
||||||
style={{ "--gap": spacing } as React.CSSProperties}
|
|
||||||
className={cn(
|
|
||||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
|
||||||
{children}
|
|
||||||
</ToggleGroupContext.Provider>
|
|
||||||
</ToggleGroupPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToggleGroupItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
|
||||||
VariantProps<typeof toggleVariants>) {
|
|
||||||
const context = React.useContext(ToggleGroupContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToggleGroupPrimitive.Item
|
|
||||||
data-slot="toggle-group-item"
|
|
||||||
data-variant={context.variant || variant}
|
|
||||||
data-size={context.size || size}
|
|
||||||
data-spacing={context.spacing}
|
|
||||||
className={cn(
|
|
||||||
toggleVariants({
|
|
||||||
variant: context.variant || variant,
|
|
||||||
size: context.size || size,
|
|
||||||
}),
|
|
||||||
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
|
||||||
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ToggleGroupPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem }
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const toggleVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
outline:
|
|
||||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-2 min-w-9",
|
|
||||||
sm: "h-8 px-1.5 min-w-8",
|
|
||||||
lg: "h-10 px-2.5 min-w-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Toggle({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
|
||||||
VariantProps<typeof toggleVariants>) {
|
|
||||||
return (
|
|
||||||
<TogglePrimitive.Root
|
|
||||||
data-slot="toggle"
|
|
||||||
className={cn(toggleVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Toggle, toggleVariants }
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function TooltipProvider({
|
|
||||||
delayDuration = 0,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Provider
|
|
||||||
data-slot="tooltip-provider"
|
|
||||||
delayDuration={delayDuration}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tooltip({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
||||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 0,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
data-slot="tooltip-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
||||||
</TooltipPrimitive.Content>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
export function CardSkeleton() {
|
|
||||||
return (
|
|
||||||
<Skeleton
|
|
||||||
className="gap-6 rounded-xl py-6 h-full"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { axiosClient } from '@/lib/axios';
|
|
||||||
|
|
||||||
export function useCheckinCode(eventId: string, enabled: boolean) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['getCheckinCode', eventId],
|
|
||||||
queryFn: async () => {
|
|
||||||
return axiosClient.get<{
|
|
||||||
checkin_code: string;
|
|
||||||
}>('/user/checkin', {
|
|
||||||
params: {
|
|
||||||
event_id: eventId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
enabled,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { AuthorizeSearchParams } from '@/routes/authorize';
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
import { axiosClient } from '@/lib/axios';
|
|
||||||
|
|
||||||
interface GetMagicLinkPayload extends AuthorizeSearchParams {
|
|
||||||
email: string;
|
|
||||||
turnstile_token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGetMagicLink() {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (payload: GetMagicLinkPayload) => {
|
|
||||||
return axiosClient.post<{ status: string }>('/auth/magic', payload);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
||||||
import { axiosClient } from '@/lib/axios';
|
|
||||||
|
|
||||||
export function useUserInfo() {
|
|
||||||
return useSuspenseQuery({
|
|
||||||
queryKey: ['userInfo'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await axiosClient.get<{
|
|
||||||
user_id: string;
|
|
||||||
email: string;
|
|
||||||
type: string;
|
|
||||||
nickname: string;
|
|
||||||
subtitle: string;
|
|
||||||
avatar: string;
|
|
||||||
bio: string;
|
|
||||||
}
|
|
||||||
>('/user/info');
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
staleTime: 10 * 60 * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
||||||
import { axiosClient } from '@/lib/axios';
|
|
||||||
|
|
||||||
export function useValidateMagicLink(ticket: string) {
|
|
||||||
return useSuspenseQuery({
|
|
||||||
queryKey: ['validateMagicLink', ticket],
|
|
||||||
queryFn: async () => {
|
|
||||||
return axiosClient.get<{ access_token: string; refresh_token: string }>('/auth/magic/verify', { params: { token: ticket } });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
|
||||||
|
|
||||||
export function useIsMobile() {
|
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
||||||
const onChange = () => {
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
||||||
};
|
|
||||||
mql.addEventListener('change', onChange);
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
||||||
return () => mql.removeEventListener('change', onChange);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return !!isMobile;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { clearTokens } from '@/lib/token';
|
|
||||||
|
|
||||||
export function useLogout() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
clearTokens();
|
|
||||||
void navigate({ to: '/authorize' });
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
return { logout };
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { createContext, use } from 'react';
|
|
||||||
|
|
||||||
export type Theme = 'dark' | 'light' | 'system';
|
|
||||||
|
|
||||||
interface ThemeProviderState {
|
|
||||||
theme: Theme;
|
|
||||||
setTheme: (theme: Theme) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
|
||||||
theme: 'system',
|
|
||||||
setTheme: () => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
const context = use(ThemeProviderContext);
|
|
||||||
|
|
||||||
if (context === undefined)
|
|
||||||
throw new Error('useTheme must be used within a ThemeProvider');
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&display=swap");
|
|
||||||
@import "tailwindcss";
|
|
||||||
@plugin "@tailwindcss/typography";
|
|
||||||
@import "tw-animate-css";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--radius-2xl: calc(var(--radius) + 8px);
|
|
||||||
--radius-3xl: calc(var(--radius) + 12px);
|
|
||||||
--radius-4xl: calc(var(--radius) + 16px);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--font-sans: "Inter", sans-serif;
|
|
||||||
--font-mono: "Noto Sans Mono", monospace;
|
|
||||||
--font-serif: "Lora", serif;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
|
||||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
|
||||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
|
||||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
|
||||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
|
||||||
--tracking-normal: var(--tracking-normal);
|
|
||||||
--shadow-2xl: var(--shadow-2xl);
|
|
||||||
--shadow-xl: var(--shadow-xl);
|
|
||||||
--shadow-lg: var(--shadow-lg);
|
|
||||||
--shadow-md: var(--shadow-md);
|
|
||||||
--shadow: var(--shadow);
|
|
||||||
--shadow-sm: var(--shadow-sm);
|
|
||||||
--shadow-xs: var(--shadow-xs);
|
|
||||||
--shadow-2xs: var(--shadow-2xs);
|
|
||||||
--spacing: var(--spacing);
|
|
||||||
--letter-spacing: var(--letter-spacing);
|
|
||||||
--shadow-offset-y: var(--shadow-offset-y);
|
|
||||||
--shadow-offset-x: var(--shadow-offset-x);
|
|
||||||
--shadow-spread: var(--shadow-spread);
|
|
||||||
--shadow-blur: var(--shadow-blur);
|
|
||||||
--shadow-opacity: var(--shadow-opacity);
|
|
||||||
--color-shadow-color: var(--shadow-color);
|
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--background: oklch(0.9816 0.0017 247.839);
|
|
||||||
--foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--popover: oklch(1 0 0);
|
|
||||||
--popover-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--primary: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--primary-foreground: oklch(1 0 0);
|
|
||||||
--secondary: oklch(0.7499 0.0898 239.3977);
|
|
||||||
--secondary-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--muted: oklch(0.9417 0.0052 247.879);
|
|
||||||
--muted-foreground: oklch(0.5575 0.0165 244.8933);
|
|
||||||
--accent: oklch(0.9417 0.0052 247.879);
|
|
||||||
--accent-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--destructive: oklch(0.5915 0.202 21.2388);
|
|
||||||
--border: oklch(0.9109 0.007 247.9014);
|
|
||||||
--input: oklch(1 0 0);
|
|
||||||
--ring: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--chart-1: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--chart-2: oklch(0.7499 0.0898 239.3977);
|
|
||||||
--chart-3: oklch(0.4711 0.0998 264.0792);
|
|
||||||
--chart-4: oklch(0.6689 0.0699 240.3096);
|
|
||||||
--chart-5: oklch(0.5107 0.1098 263.6921);
|
|
||||||
--sidebar: oklch(0.9632 0.0034 247.8585);
|
|
||||||
--sidebar-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--sidebar-primary: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--sidebar-primary-foreground: oklch(1 0 0);
|
|
||||||
--sidebar-accent: oklch(0.9417 0.0052 247.879);
|
|
||||||
--sidebar-accent-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--sidebar-border: oklch(0.9109 0.007 247.9014);
|
|
||||||
--sidebar-ring: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--destructive-foreground: oklch(1 0 0);
|
|
||||||
--font-sans: "Inter", sans-serif;
|
|
||||||
--font-serif: "Lora", serif;
|
|
||||||
--font-mono: "JetBrains Mono", monospace;
|
|
||||||
--shadow-color: #000000;
|
|
||||||
--shadow-opacity: 0.05;
|
|
||||||
--shadow-blur: 0.5rem;
|
|
||||||
--shadow-spread: 0rem;
|
|
||||||
--shadow-offset-x: 0rem;
|
|
||||||
--shadow-offset-y: 0.1rem;
|
|
||||||
--letter-spacing: 0em;
|
|
||||||
--spacing: 0.25rem;
|
|
||||||
--shadow-2xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.03);
|
|
||||||
--shadow-xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.03);
|
|
||||||
--shadow-sm:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05),
|
|
||||||
0rem 1px 2px -1px hsl(0 0% 0% / 0.05);
|
|
||||||
--shadow:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05),
|
|
||||||
0rem 1px 2px -1px hsl(0 0% 0% / 0.05);
|
|
||||||
--shadow-md:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05),
|
|
||||||
0rem 2px 4px -1px hsl(0 0% 0% / 0.05);
|
|
||||||
--shadow-lg:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05),
|
|
||||||
0rem 4px 6px -1px hsl(0 0% 0% / 0.05);
|
|
||||||
--shadow-xl:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05),
|
|
||||||
0rem 8px 10px -1px hsl(0 0% 0% / 0.05);
|
|
||||||
--shadow-2xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.13);
|
|
||||||
--tracking-normal: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.227 0.012 270.8402);
|
|
||||||
--foreground: oklch(0.9067 0 0);
|
|
||||||
--card: oklch(0.263 0.0127 258.3724);
|
|
||||||
--card-foreground: oklch(0.9067 0 0);
|
|
||||||
--popover: oklch(0.263 0.0127 258.3724);
|
|
||||||
--popover-foreground: oklch(0.9067 0 0);
|
|
||||||
--primary: oklch(0.5774 0.1248 263.377);
|
|
||||||
--primary-foreground: oklch(1 0 0);
|
|
||||||
--secondary: oklch(0.7636 0.0866 239.8852);
|
|
||||||
--secondary-foreground: oklch(0.2621 0.0095 248.1897);
|
|
||||||
--muted: oklch(0.3006 0.0156 264.3078);
|
|
||||||
--muted-foreground: oklch(0.7137 0.0192 261.3246);
|
|
||||||
--accent: oklch(0.3006 0.0156 264.3078);
|
|
||||||
--accent-foreground: oklch(0.9067 0 0);
|
|
||||||
--destructive: oklch(0.5915 0.202 21.2388);
|
|
||||||
--border: oklch(0.3451 0.0133 248.2124);
|
|
||||||
--input: oklch(0.263 0.0127 258.3724);
|
|
||||||
--ring: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--chart-1: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--chart-2: oklch(0.7499 0.0898 239.3977);
|
|
||||||
--chart-3: oklch(0.4711 0.0998 264.0792);
|
|
||||||
--chart-4: oklch(0.6689 0.0699 240.3096);
|
|
||||||
--chart-5: oklch(0.5107 0.1098 263.6921);
|
|
||||||
--sidebar: oklch(0.227 0.012 270.8402);
|
|
||||||
--sidebar-foreground: oklch(0.9067 0 0);
|
|
||||||
--sidebar-primary: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--sidebar-primary-foreground: oklch(1 0 0);
|
|
||||||
--sidebar-accent: oklch(0.3006 0.0156 264.3078);
|
|
||||||
--sidebar-accent-foreground: oklch(0.9067 0 0);
|
|
||||||
--sidebar-border: oklch(0.3451 0.0133 248.2124);
|
|
||||||
--sidebar-ring: oklch(0.5502 0.1193 263.8209);
|
|
||||||
--destructive-foreground: oklch(1 0 0);
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--font-sans: "Inter", sans-serif;
|
|
||||||
--font-serif: "Lora", serif;
|
|
||||||
--font-mono: "JetBrains Mono", monospace;
|
|
||||||
--shadow-color: #000000;
|
|
||||||
--shadow-opacity: 0.3;
|
|
||||||
--shadow-blur: 0.5rem;
|
|
||||||
--shadow-spread: 0rem;
|
|
||||||
--shadow-offset-x: 0rem;
|
|
||||||
--shadow-offset-y: 0.1rem;
|
|
||||||
--letter-spacing: 0em;
|
|
||||||
--spacing: 0.25rem;
|
|
||||||
--shadow-2xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.15);
|
|
||||||
--shadow-xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.15);
|
|
||||||
--shadow-sm:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.3),
|
|
||||||
0rem 1px 2px -1px hsl(0 0% 0% / 0.3);
|
|
||||||
--shadow:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.3),
|
|
||||||
0rem 1px 2px -1px hsl(0 0% 0% / 0.3);
|
|
||||||
--shadow-md:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.3),
|
|
||||||
0rem 2px 4px -1px hsl(0 0% 0% / 0.3);
|
|
||||||
--shadow-lg:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.3),
|
|
||||||
0rem 4px 6px -1px hsl(0 0% 0% / 0.3);
|
|
||||||
--shadow-xl:
|
|
||||||
0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.3),
|
|
||||||
0rem 8px 10px -1px hsl(0 0% 0% / 0.3);
|
|
||||||
--shadow-2xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
letter-spacing: var(--tracking-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="dialog-overlay"] {
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import type { AxiosRequestConfig } from 'axios';
|
|
||||||
import axios, { AxiosError } from 'axios';
|
|
||||||
import { router } from '@/lib/router';
|
|
||||||
import { doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token';
|
|
||||||
|
|
||||||
export const axiosClient = axios.create({
|
|
||||||
baseURL: '/api/v1/',
|
|
||||||
});
|
|
||||||
|
|
||||||
axiosClient.interceptors.request.use((config) => {
|
|
||||||
const token = getToken();
|
|
||||||
if (token !== null) {
|
|
||||||
config.headers = config.headers ?? {};
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
type RetryConfig = AxiosRequestConfig & { _retry?: boolean };
|
|
||||||
|
|
||||||
axiosClient.interceptors.response.use(undefined, async (error: AxiosError) => {
|
|
||||||
const originalRequest = error.config as RetryConfig | undefined;
|
|
||||||
if (!error.response || error.response.status !== 401 || !originalRequest) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.response.status === 401 && getRefreshToken() !== null) {
|
|
||||||
try {
|
|
||||||
const maybeRefreshTokenValue = await doRefreshToken();
|
|
||||||
const { access_token, refresh_token } = maybeRefreshTokenValue.data;
|
|
||||||
originalRequest.headers = originalRequest.headers ?? {};
|
|
||||||
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
|
||||||
setToken(access_token);
|
|
||||||
setRefreshToken(refresh_token);
|
|
||||||
return await axiosClient(originalRequest);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (e instanceof AxiosError && e.status === 401) {
|
|
||||||
await router.navigate({ to: '/authorize' });
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import {
|
|
||||||
IconDashboard,
|
|
||||||
IconUser,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
|
|
||||||
export const navData = {
|
|
||||||
navMain: [
|
|
||||||
{
|
|
||||||
title: '工作台',
|
|
||||||
url: '/',
|
|
||||||
icon: IconDashboard,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navSecondary: [
|
|
||||||
{
|
|
||||||
title: '个人资料',
|
|
||||||
url: '/profile',
|
|
||||||
icon: IconUser,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generate a cryptographically secure OAuth2 state string
|
|
||||||
* base64url encoded, URL-safe
|
|
||||||
*/
|
|
||||||
export function generateOAuthState(bytes: number = 32): string {
|
|
||||||
const random = new Uint8Array(bytes);
|
|
||||||
crypto.getRandomValues(random);
|
|
||||||
|
|
||||||
// base64url encode
|
|
||||||
return btoa(String.fromCharCode(...random))
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '');
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { createRouter } from '@tanstack/react-router';
|
|
||||||
// Import the generated route tree
|
|
||||||
import { routeTree } from '../routeTree.gen';
|
|
||||||
|
|
||||||
// Create a new router instance
|
|
||||||
export const router = createRouter({ routeTree });
|
|
||||||
|
|
||||||
// Register the router instance for type safety
|
|
||||||
declare module '@tanstack/react-router' {
|
|
||||||
interface Register {
|
|
||||||
router: typeof router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export function setToken(token: string) {
|
|
||||||
localStorage.setItem('token', token);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getToken() {
|
|
||||||
return localStorage.getItem('token');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeToken() {
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasToken() {
|
|
||||||
return getToken() !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRefreshToken(refreshToken: string) {
|
|
||||||
localStorage.setItem('refreshToken', refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRefreshToken() {
|
|
||||||
return localStorage.getItem('refreshToken');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearTokens() {
|
|
||||||
removeToken();
|
|
||||||
setRefreshToken('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function doSetTokenByCode(code: string) {
|
|
||||||
const { data } = await axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/token', { code });
|
|
||||||
setToken(data.access_token);
|
|
||||||
setRefreshToken(data.refresh_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function doRefreshToken() {
|
|
||||||
return axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/refresh', { refresh_token: getRefreshToken() });
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { ClassValue } from 'clsx';
|
|
||||||
// eslint-disable-next-line unicorn/prefer-node-protocol
|
|
||||||
import { Buffer } from 'buffer';
|
|
||||||
import { clsx } from 'clsx';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function base64ToUtf8(base64: string): string {
|
|
||||||
return new TextDecoder('utf-8').decode(
|
|
||||||
Uint8Array.from(Buffer.from(base64, 'base64')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function utf8ToBase64(utf8: string): string {
|
|
||||||
return Buffer.from(utf8, 'utf-8').toString('base64');
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { RouterProvider } from '@tanstack/react-router';
|
|
||||||
import { StrictMode } from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import { router } from '@/lib/router';
|
|
||||||
|
|
||||||
// Render the app
|
|
||||||
const rootElement = document.getElementById('root')!;
|
|
||||||
if (!rootElement.innerHTML) {
|
|
||||||
const root = ReactDOM.createRoot(rootElement);
|
|
||||||
root.render(
|
|
||||||
<StrictMode>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
|
|
||||||
// This file was automatically generated by TanStack Router.
|
|
||||||
// You should NOT make any changes in this file as it will be overwritten.
|
|
||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
|
||||||
import { Route as TokenRouteImport } from './routes/token'
|
|
||||||
import { Route as MagicLinkSentRouteImport } from './routes/magicLinkSent'
|
|
||||||
import { Route as AuthorizeRouteImport } from './routes/authorize'
|
|
||||||
import { Route as SidebarLayoutRouteImport } from './routes/_sidebarLayout'
|
|
||||||
import { Route as SidebarLayoutIndexRouteImport } from './routes/_sidebarLayout/index'
|
|
||||||
import { Route as SidebarLayoutProfileRouteImport } from './routes/_sidebarLayout/profile'
|
|
||||||
|
|
||||||
const TokenRoute = TokenRouteImport.update({
|
|
||||||
id: '/token',
|
|
||||||
path: '/token',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const MagicLinkSentRoute = MagicLinkSentRouteImport.update({
|
|
||||||
id: '/magicLinkSent',
|
|
||||||
path: '/magicLinkSent',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const AuthorizeRoute = AuthorizeRouteImport.update({
|
|
||||||
id: '/authorize',
|
|
||||||
path: '/authorize',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const SidebarLayoutRoute = SidebarLayoutRouteImport.update({
|
|
||||||
id: '/_sidebarLayout',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const SidebarLayoutIndexRoute = SidebarLayoutIndexRouteImport.update({
|
|
||||||
id: '/',
|
|
||||||
path: '/',
|
|
||||||
getParentRoute: () => SidebarLayoutRoute,
|
|
||||||
} as any)
|
|
||||||
const SidebarLayoutProfileRoute = SidebarLayoutProfileRouteImport.update({
|
|
||||||
id: '/profile',
|
|
||||||
path: '/profile',
|
|
||||||
getParentRoute: () => SidebarLayoutRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
|
||||||
'/': typeof SidebarLayoutIndexRoute
|
|
||||||
'/authorize': typeof AuthorizeRoute
|
|
||||||
'/magicLinkSent': typeof MagicLinkSentRoute
|
|
||||||
'/token': typeof TokenRoute
|
|
||||||
'/profile': typeof SidebarLayoutProfileRoute
|
|
||||||
}
|
|
||||||
export interface FileRoutesByTo {
|
|
||||||
'/authorize': typeof AuthorizeRoute
|
|
||||||
'/magicLinkSent': typeof MagicLinkSentRoute
|
|
||||||
'/token': typeof TokenRoute
|
|
||||||
'/profile': typeof SidebarLayoutProfileRoute
|
|
||||||
'/': typeof SidebarLayoutIndexRoute
|
|
||||||
}
|
|
||||||
export interface FileRoutesById {
|
|
||||||
__root__: typeof rootRouteImport
|
|
||||||
'/_sidebarLayout': typeof SidebarLayoutRouteWithChildren
|
|
||||||
'/authorize': typeof AuthorizeRoute
|
|
||||||
'/magicLinkSent': typeof MagicLinkSentRoute
|
|
||||||
'/token': typeof TokenRoute
|
|
||||||
'/_sidebarLayout/profile': typeof SidebarLayoutProfileRoute
|
|
||||||
'/_sidebarLayout/': typeof SidebarLayoutIndexRoute
|
|
||||||
}
|
|
||||||
export interface FileRouteTypes {
|
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
|
||||||
fullPaths: '/' | '/authorize' | '/magicLinkSent' | '/token' | '/profile'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
|
||||||
to: '/authorize' | '/magicLinkSent' | '/token' | '/profile' | '/'
|
|
||||||
id:
|
|
||||||
| '__root__'
|
|
||||||
| '/_sidebarLayout'
|
|
||||||
| '/authorize'
|
|
||||||
| '/magicLinkSent'
|
|
||||||
| '/token'
|
|
||||||
| '/_sidebarLayout/profile'
|
|
||||||
| '/_sidebarLayout/'
|
|
||||||
fileRoutesById: FileRoutesById
|
|
||||||
}
|
|
||||||
export interface RootRouteChildren {
|
|
||||||
SidebarLayoutRoute: typeof SidebarLayoutRouteWithChildren
|
|
||||||
AuthorizeRoute: typeof AuthorizeRoute
|
|
||||||
MagicLinkSentRoute: typeof MagicLinkSentRoute
|
|
||||||
TokenRoute: typeof TokenRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
|
||||||
interface FileRoutesByPath {
|
|
||||||
'/token': {
|
|
||||||
id: '/token'
|
|
||||||
path: '/token'
|
|
||||||
fullPath: '/token'
|
|
||||||
preLoaderRoute: typeof TokenRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/magicLinkSent': {
|
|
||||||
id: '/magicLinkSent'
|
|
||||||
path: '/magicLinkSent'
|
|
||||||
fullPath: '/magicLinkSent'
|
|
||||||
preLoaderRoute: typeof MagicLinkSentRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/authorize': {
|
|
||||||
id: '/authorize'
|
|
||||||
path: '/authorize'
|
|
||||||
fullPath: '/authorize'
|
|
||||||
preLoaderRoute: typeof AuthorizeRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/_sidebarLayout': {
|
|
||||||
id: '/_sidebarLayout'
|
|
||||||
path: ''
|
|
||||||
fullPath: '/'
|
|
||||||
preLoaderRoute: typeof SidebarLayoutRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/_sidebarLayout/': {
|
|
||||||
id: '/_sidebarLayout/'
|
|
||||||
path: '/'
|
|
||||||
fullPath: '/'
|
|
||||||
preLoaderRoute: typeof SidebarLayoutIndexRouteImport
|
|
||||||
parentRoute: typeof SidebarLayoutRoute
|
|
||||||
}
|
|
||||||
'/_sidebarLayout/profile': {
|
|
||||||
id: '/_sidebarLayout/profile'
|
|
||||||
path: '/profile'
|
|
||||||
fullPath: '/profile'
|
|
||||||
preLoaderRoute: typeof SidebarLayoutProfileRouteImport
|
|
||||||
parentRoute: typeof SidebarLayoutRoute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SidebarLayoutRouteChildren {
|
|
||||||
SidebarLayoutProfileRoute: typeof SidebarLayoutProfileRoute
|
|
||||||
SidebarLayoutIndexRoute: typeof SidebarLayoutIndexRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarLayoutRouteChildren: SidebarLayoutRouteChildren = {
|
|
||||||
SidebarLayoutProfileRoute: SidebarLayoutProfileRoute,
|
|
||||||
SidebarLayoutIndexRoute: SidebarLayoutIndexRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarLayoutRouteWithChildren = SidebarLayoutRoute._addFileChildren(
|
|
||||||
SidebarLayoutRouteChildren,
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
|
||||||
SidebarLayoutRoute: SidebarLayoutRouteWithChildren,
|
|
||||||
AuthorizeRoute: AuthorizeRoute,
|
|
||||||
MagicLinkSentRoute: MagicLinkSentRoute,
|
|
||||||
TokenRoute: TokenRoute,
|
|
||||||
}
|
|
||||||
export const routeTree = rootRouteImport
|
|
||||||
._addFileChildren(rootRouteChildren)
|
|
||||||
._addFileTypes<FileRouteTypes>()
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
|
||||||
import '@/index.css';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: (failureCount, error: any) => {
|
|
||||||
// eslint-disable-next-line ts/no-unsafe-assignment
|
|
||||||
const status
|
|
||||||
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
||||||
= error?.response?.status ?? error?.status;
|
|
||||||
|
|
||||||
if (status >= 400 && status < 500) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return failureCount < 3;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function RootLayout() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Outlet />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
<Toaster position="top-right" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createRootRoute({ component: RootLayout });
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
|
||||||
import { AppSidebar } from '@/components/sidebar/app-sidebar';
|
|
||||||
import { SiteHeader } from '@/components/site-header';
|
|
||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_sidebarLayout')({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return (
|
|
||||||
<SidebarProvider
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
|
||||||
'--header-height': 'calc(var(--spacing) * 12)',
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AppSidebar variant="inset" />
|
|
||||||
<SidebarInset>
|
|
||||||
<SiteHeader />
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarInset>
|
|
||||||
</SidebarProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
|
||||||
import { hasToken } from '@/lib/token';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_sidebarLayout/')({
|
|
||||||
component: Index,
|
|
||||||
loader: async () => {
|
|
||||||
if (!hasToken()) {
|
|
||||||
throw redirect({
|
|
||||||
to: '/authorize',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function Index() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
|
||||||
{/* Section Cards */}
|
|
||||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card
|
|
||||||
grid grid-cols-1 gap-4 px-4 auto-rows-[minmax(175px,auto)] items-stretch
|
|
||||||
*:data-[slot=card]:bg-linear-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router';
|
|
||||||
import { MainProfile } from '@/components/profile/main-profile';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_sidebarLayout/profile')({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[560px] flex-col gap-6 px-4 py-6">
|
|
||||||
<MainProfile />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router';
|
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
|
||||||
import z from 'zod';
|
|
||||||
import { LoginForm } from '@/components/login-form';
|
|
||||||
import { generateOAuthState } from '@/lib/random';
|
|
||||||
import { getToken } from '@/lib/token';
|
|
||||||
|
|
||||||
const authorizeSchema = z.object({
|
|
||||||
response_type: z.literal('code').default('code'),
|
|
||||||
client_id: z.literal('org_client').default('org_client'),
|
|
||||||
redirect_uri: z.string().default(`${new URL(import.meta.env.VITE_APP_BASE_URL as string).toString()}token`),
|
|
||||||
state: z.string().default(generateOAuthState()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AuthorizeSearchParams = z.infer<typeof authorizeSchema>;
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/authorize')({
|
|
||||||
component: RouteComponent,
|
|
||||||
validateSearch: zodValidator(authorizeSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const token = getToken();
|
|
||||||
const oauthParams = Route.useSearch();
|
|
||||||
if (token !== null) {
|
|
||||||
const base = new URL(window.location.origin);
|
|
||||||
const url = new URL('/api/v1/auth/redirect', base);
|
|
||||||
url.searchParams.set('client_id', oauthParams.client_id);
|
|
||||||
url.searchParams.set('response_type', oauthParams.response_type);
|
|
||||||
url.searchParams.set('redirect_uri', oauthParams.redirect_uri);
|
|
||||||
url.searchParams.set('state', oauthParams.state);
|
|
||||||
window.location.href = url.toString();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
|
||||||
<div className="w-full max-w-sm">
|
|
||||||
<LoginForm oauthParams={oauthParams} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { createFileRoute, Navigate } from '@tanstack/react-router';
|
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
|
||||||
import z from 'zod';
|
|
||||||
import NixOSLogo from '@/assets/nixos.svg?react';
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
email: z.string().optional(),
|
|
||||||
});
|
|
||||||
export const Route = createFileRoute('/magicLinkSent')({
|
|
||||||
component: RouteComponent,
|
|
||||||
validateSearch: zodValidator(paramsSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const { email } = Route.useSearch();
|
|
||||||
return email !== undefined
|
|
||||||
? (
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
bg-background flex min-h-svh flex-row items-center justify-center gap-6 p-6 md:p-10"
|
|
||||||
>
|
|
||||||
<NixOSLogo className="size-12" />
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mx-2 inline-block h-6 w-px bg-current opacity-40"
|
|
||||||
/>
|
|
||||||
登录链接已发送至
|
|
||||||
{' '}
|
|
||||||
{email}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<Navigate to="/authorize" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import z from 'zod';
|
|
||||||
import { doSetTokenByCode } from '@/lib/token';
|
|
||||||
|
|
||||||
const tokenCodeSchema = z.object({
|
|
||||||
code: z.string().nonempty(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/token')({
|
|
||||||
component: RouteComponent,
|
|
||||||
validateSearch: tokenCodeSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const { code } = Route.useSearch();
|
|
||||||
const [status, setStatus] = useState('Loading...');
|
|
||||||
const navigate = useNavigate();
|
|
||||||
doSetTokenByCode(code).then(() => {
|
|
||||||
void navigate({ to: '/' });
|
|
||||||
}).catch((_) => {
|
|
||||||
setStatus('Error getting token');
|
|
||||||
});
|
|
||||||
return <div>{status}</div>;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2022",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"module": "ESNext",
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
},
|
|
||||||
"types": ["vite/client", "vite-plugin-svgr/client"],
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
],
|
|
||||||
"files": []
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"module": "ESNext",
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"types": ["node"],
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user