diff --git a/backend/go.mod b/backend/go.mod
index 1280452..c211bb4 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -5,25 +5,37 @@ go 1.23.5
require (
github.com/danielgtaylor/huma/v2 v2.28.0
github.com/go-chi/chi/v5 v5.2.1
+ github.com/go-chi/jwtauth/v5 v5.3.2
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
+ github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2
github.com/joho/godotenv v1.5.1
+ github.com/lestrrat-go/jwx/v2 v2.1.3
github.com/pressly/goose/v3 v3.24.1
github.com/spf13/cobra v1.8.1
)
require (
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
+ github.com/goccy/go-json v0.10.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/lestrrat-go/blackmagic v1.0.2 // indirect
+ github.com/lestrrat-go/httpcc v1.0.1 // indirect
+ github.com/lestrrat-go/httprc v1.0.6 // indirect
+ github.com/lestrrat-go/iter v1.0.2 // indirect
+ github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
+ github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
diff --git a/backend/go.sum b/backend/go.sum
index 8459453..c1e8c76 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -6,12 +6,20 @@ github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wX
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-chi/jwtauth/v5 v5.3.2 h1:s+ON3ATyyMs3Me0kqyuua6Rwu+2zqIIkL0GCaMarwvs=
+github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -28,6 +36,18 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
+github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
+github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
+github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
+github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
@@ -41,6 +61,8 @@ github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -51,7 +73,9 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go
index 2e4e5ee..efc9818 100644
--- a/backend/internal/api/api.go
+++ b/backend/internal/api/api.go
@@ -10,20 +10,31 @@ import (
"github.com/danielgtaylor/huma/v2/adapters/humachi"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
+ "github.com/jackc/pgx/v5/pgxpool"
+
+ "github.com/forfarm/backend/internal/domain"
+ m "github.com/forfarm/backend/internal/middlewares"
+ "github.com/forfarm/backend/internal/repository"
)
type api struct {
logger *slog.Logger
httpClient *http.Client
+
+ userRepo domain.UserRepository
}
-func NewAPI(ctx context.Context, logger *slog.Logger) *api {
+func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api {
client := &http.Client{}
+ userRepository := repository.NewPostgresUser(pool)
+
return &api{
logger: logger,
httpClient: client,
+
+ userRepo: userRepository,
}
}
@@ -34,14 +45,20 @@ func (a *api) Server(port int) *http.Server {
}
func (a *api) Routes() *chi.Mux {
- r := chi.NewRouter()
+ router := chi.NewRouter()
+ router.Use(middleware.Logger)
- r.Use(middleware.Logger)
+ config := huma.DefaultConfig("ForFarm Public API", "v1.0.0")
+ api := humachi.New(router, config)
- api := humachi.New(r, huma.DefaultConfig("ForFarm API", "v1.0.0"))
- huma.Get(api, "/helloworld", a.helloWorldHandler)
+ router.Group(func(r chi.Router) {
+ a.registerAuthRoutes(r, api)
+ })
- // r.Get("/helloworld", a.helloWorldHandler)
+ router.Group(func(r chi.Router) {
+ api.UseMiddleware(m.AuthMiddleware(api))
+ a.registerHelloRoutes(r, api)
+ })
- return r
+ return router
}
diff --git a/backend/internal/api/auth.go b/backend/internal/api/auth.go
new file mode 100644
index 0000000..b202f83
--- /dev/null
+++ b/backend/internal/api/auth.go
@@ -0,0 +1,115 @@
+package api
+
+import (
+ "context"
+ "errors"
+ "net/http"
+
+ "github.com/danielgtaylor/huma/v2"
+ "github.com/forfarm/backend/internal/domain"
+ "github.com/forfarm/backend/internal/utilities"
+ "github.com/go-chi/chi/v5"
+ "golang.org/x/crypto/bcrypt"
+)
+
+func (a *api) registerAuthRoutes(_ chi.Router, api huma.API) {
+ tags := []string{"auth"}
+
+ prefix := "/auth"
+
+ huma.Register(api, huma.Operation{
+ OperationID: "register",
+ Method: http.MethodPost,
+ Path: prefix + "/register",
+ Tags: tags,
+ }, a.registerHandler)
+
+ huma.Register(api, huma.Operation{
+ OperationID: "login",
+ Method: http.MethodPost,
+ Path: prefix + "/login",
+ Tags: tags,
+ }, a.loginHandler)
+}
+
+type LoginInput struct {
+ Body struct {
+ Email string `json:"email" example:" Email address of the user"`
+ Password string `json:"password" example:" Password of the user"`
+ }
+}
+
+type LoginOutput struct {
+ Body struct {
+ Token string `json:"token" example:" JWT token for the user"`
+ }
+}
+
+type RegisterInput struct {
+ Body struct {
+ Email string `json:"email" example:" Email address of the user"`
+ Password string `json:"password" example:" Password of the user"`
+ }
+}
+
+type RegisterOutput struct {
+ Body struct {
+ Token string `json:"token" example:" JWT token for the user"`
+ }
+}
+
+func (a *api) registerHandler(ctx context.Context, input *RegisterInput) (*RegisterOutput, error) {
+ resp := &RegisterOutput{}
+
+ // TODO: Validate input data
+
+ _, err := a.userRepo.GetByEmail(ctx, input.Body.Email)
+ if err == domain.ErrNotFound {
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Body.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return nil, err
+ }
+
+ err = a.userRepo.CreateOrUpdate(ctx, &domain.User{
+ Email: input.Body.Email,
+ Password: string(hashedPassword),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ token, err := utilities.CreateJwtToken(input.Body.Email)
+ if err != nil {
+ return nil, err
+ }
+
+ resp.Body.Token = token
+ return resp, nil
+ }
+
+ return nil, errors.New("user already exists")
+}
+
+func (a *api) loginHandler(ctx context.Context, input *LoginInput) (*LoginOutput, error) {
+ resp := &LoginOutput{}
+
+ // TODO: Validate input data
+
+ user, err := a.userRepo.GetByEmail(ctx, input.Body.Email)
+ if err != nil {
+ return nil, err
+ }
+
+ // verify password hash
+ if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Body.Password)); err != nil {
+ return nil, err
+ }
+
+ token, err := utilities.CreateJwtToken(user.UUID)
+ if err != nil {
+ return nil, err
+ }
+
+ resp.Body.Token = token
+ return resp, nil
+}
diff --git a/backend/internal/api/helloworld.go b/backend/internal/api/helloworld.go
index a1d2664..df72e79 100644
--- a/backend/internal/api/helloworld.go
+++ b/backend/internal/api/helloworld.go
@@ -2,16 +2,37 @@ package api
import (
"context"
+ "net/http"
+
+ "github.com/danielgtaylor/huma/v2"
+ "github.com/go-chi/chi/v5"
)
-type HelloworldOutput struct {
+type HelloWorldInput struct {
+ MyHeader string `header:"Authorization" required:"true" example:"Bearer token"`
+}
+
+type HelloWorldOutput struct {
Body struct {
- Message string `json:"message" example:"Hello, world!" doc:"Greeting message"`
+ Message string `json:"message" example:"hello world"`
}
}
-func (a *api) helloWorldHandler(ctx context.Context, input *struct{}) (*HelloworldOutput, error) {
- resp := &HelloworldOutput{}
- resp.Body.Message = "Hello, world!"
+func (a *api) registerHelloRoutes(_ chi.Router, api huma.API) {
+ tags := []string{"hello"}
+
+ huma.Register(api, huma.Operation{
+ OperationID: "helloWorld",
+ Method: http.MethodPost,
+ Path: "/hello",
+ Tags: tags,
+ Summary: "Get hello world message",
+ Description: "Returns a simple hello world message",
+ }, a.helloWorldHandler)
+}
+
+func (a *api) helloWorldHandler(ctx context.Context, input *HelloWorldInput) (*HelloWorldOutput, error) {
+ resp := &HelloWorldOutput{}
+ resp.Body.Message = "hello world from forfarm"
return resp, nil
}
diff --git a/backend/internal/cmd/api.go b/backend/internal/cmd/api.go
index 2855e7a..816b457 100644
--- a/backend/internal/cmd/api.go
+++ b/backend/internal/cmd/api.go
@@ -29,7 +29,7 @@ func APICmd(ctx context.Context) *cobra.Command {
}
defer pool.Close()
- api := api.NewAPI(ctx, logger)
+ api := api.NewAPI(ctx, logger, pool)
server := api.Server(port)
go func() {
diff --git a/backend/internal/cmd/root.go b/backend/internal/cmd/root.go
index af7086a..9dd0d88 100644
--- a/backend/internal/cmd/root.go
+++ b/backend/internal/cmd/root.go
@@ -2,7 +2,6 @@ package cmd
import (
"context"
- "net/http"
"os"
"github.com/joho/godotenv"
@@ -20,10 +19,6 @@ func Execute(ctx context.Context) int {
rootCmd.AddCommand(APICmd(ctx))
rootCmd.AddCommand(MigrateCmd(ctx, "pgx", os.Getenv("DATABASE_URL")))
- go func() {
- _ = http.ListenAndServe("localhost:8000", nil)
- }()
-
if err := rootCmd.Execute(); err != nil {
return 1
}
diff --git a/backend/internal/domain/cropland.go b/backend/internal/domain/cropland.go
new file mode 100644
index 0000000..984e6e4
--- /dev/null
+++ b/backend/internal/domain/cropland.go
@@ -0,0 +1,36 @@
+package domain
+
+import (
+ "context"
+ "time"
+
+ validation "github.com/go-ozzo/ozzo-validation/v4"
+)
+
+type Cropland struct {
+ UUID string
+ Name string
+ Status string
+ Priority int
+ LandSize float64
+ GrowthStage string
+ PlantID string
+ FarmID string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+func (c *Cropland) Validate() error {
+ return validation.ValidateStruct(c,
+ validation.Field(&c.Name, validation.Required),
+ validation.Field(&c.Status, validation.Required),
+ validation.Field(&c.GrowthStage, validation.Required),
+ validation.Field(&c.LandSize, validation.Required),
+ )
+}
+
+type CroplandRepository interface {
+ GetByID(context.Context, string) (Cropland, error)
+ CreateOrUpdate(context.Context, *Cropland) error
+ Delete(context.Context, string) error
+}
diff --git a/backend/internal/domain/farm.go b/backend/internal/domain/farm.go
new file mode 100644
index 0000000..f631334
--- /dev/null
+++ b/backend/internal/domain/farm.go
@@ -0,0 +1,33 @@
+package domain
+
+import (
+ "context"
+ "time"
+
+ validation "github.com/go-ozzo/ozzo-validation/v4"
+)
+
+type Farm struct {
+ UUID string
+ Name string
+ Lat float64
+ Lon float64
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ OwnerID string
+}
+
+func (f *Farm) Validate() error {
+ return validation.ValidateStruct(f,
+ validation.Field(&f.Name, validation.Required),
+ validation.Field(&f.Lat, validation.Required),
+ validation.Field(&f.Lon, validation.Required),
+ validation.Field(&f.OwnerID, validation.Required),
+ )
+}
+
+type FarmRepository interface {
+ GetByID(context.Context, string) (Farm, error)
+ CreateOrUpdate(context.Context, *Farm) error
+ Delete(context.Context, string) error
+}
diff --git a/backend/internal/domain/user.go b/backend/internal/domain/user.go
index 9a2d58b..16fc939 100644
--- a/backend/internal/domain/user.go
+++ b/backend/internal/domain/user.go
@@ -27,7 +27,7 @@ func (u *User) NormalizedUsername() string {
func (u *User) Validate() error {
return validation.ValidateStruct(u,
validation.Field(&u.UUID, validation.Required),
- validation.Field(&u.Username, validation.Required, validation.Length(3, 20)),
+ validation.Field(&u.Username, validation.Length(3, 20)),
validation.Field(&u.Password, validation.Required, validation.Length(6, 100)),
validation.Field(&u.Email, validation.Required, is.Email),
)
@@ -36,6 +36,7 @@ func (u *User) Validate() error {
type UserRepository interface {
GetByID(context.Context, int64) (User, error)
GetByUsername(context.Context, string) (User, error)
+ GetByEmail(context.Context, string) (User, error)
CreateOrUpdate(context.Context, *User) error
Delete(context.Context, int64) error
}
diff --git a/backend/internal/middlewares/auth.go b/backend/internal/middlewares/auth.go
new file mode 100644
index 0000000..058e07f
--- /dev/null
+++ b/backend/internal/middlewares/auth.go
@@ -0,0 +1,35 @@
+package middlewares
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/danielgtaylor/huma/v2"
+ "github.com/forfarm/backend/internal/utilities"
+)
+
+func AuthMiddleware(api huma.API) func(ctx huma.Context, next func(huma.Context)) {
+ return func(ctx huma.Context, next func(huma.Context)) {
+ authHeader := ctx.Header("Authorization")
+ if authHeader == "" {
+ huma.WriteErr(api, ctx, http.StatusUnauthorized, "No token provided")
+ return
+ }
+
+ tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
+ if tokenStr == "" {
+ huma.WriteErr(api, ctx, http.StatusUnauthorized, "No token provided")
+ return
+ }
+
+ err := utilities.VerifyJwtToken(tokenStr)
+
+ if err != nil {
+ huma.WriteErr(api, ctx, http.StatusUnauthorized, "Invalid token")
+ return
+ }
+
+ next(ctx)
+ }
+
+}
diff --git a/backend/internal/repository/postgres_cropland.go b/backend/internal/repository/postgres_cropland.go
new file mode 100644
index 0000000..7cc48f8
--- /dev/null
+++ b/backend/internal/repository/postgres_cropland.go
@@ -0,0 +1,111 @@
+package repository
+
+import (
+ "context"
+ "strings"
+
+ "github.com/google/uuid"
+
+ "github.com/forfarm/backend/internal/domain"
+)
+
+type postgresCroplandRepository struct {
+ conn Connection
+}
+
+func NewPostgresCropland(conn Connection) domain.CroplandRepository {
+ return &postgresCroplandRepository{conn: conn}
+}
+
+func (p *postgresCroplandRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.Cropland, error) {
+ rows, err := p.conn.Query(ctx, query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var croplands []domain.Cropland
+ for rows.Next() {
+ var c domain.Cropland
+ if err := rows.Scan(
+ &c.UUID,
+ &c.Name,
+ &c.Status,
+ &c.Priority,
+ &c.LandSize,
+ &c.GrowthStage,
+ &c.PlantID,
+ &c.FarmID,
+ &c.CreatedAt,
+ &c.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ croplands = append(croplands, c)
+ }
+ return croplands, nil
+}
+
+func (p *postgresCroplandRepository) GetByID(ctx context.Context, uuid string) (domain.Cropland, error) {
+ query := `
+ SELECT uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at
+ FROM croplands
+ WHERE uuid = $1`
+
+ croplands, err := p.fetch(ctx, query, uuid)
+ if err != nil {
+ return domain.Cropland{}, err
+ }
+ if len(croplands) == 0 {
+ return domain.Cropland{}, domain.ErrNotFound
+ }
+ return croplands[0], nil
+}
+
+func (p *postgresCroplandRepository) GetByFarmID(ctx context.Context, farmID string) ([]domain.Cropland, error) {
+ query := `
+ SELECT uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at
+ FROM croplands
+ WHERE farm_id = $1`
+
+ return p.fetch(ctx, query, farmID)
+}
+
+func (p *postgresCroplandRepository) CreateOrUpdate(ctx context.Context, c *domain.Cropland) error {
+ if strings.TrimSpace(c.UUID) == "" {
+ c.UUID = uuid.New().String()
+ }
+
+ query := `
+ INSERT INTO croplands (uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
+ ON CONFLICT (uuid) DO UPDATE
+ SET name = EXCLUDED.name,
+ status = EXCLUDED.status,
+ priority = EXCLUDED.priority,
+ land_size = EXCLUDED.land_size,
+ growth_stage = EXCLUDED.growth_stage,
+ plant_id = EXCLUDED.plant_id,
+ farm_id = EXCLUDED.farm_id,
+ updated_at = NOW()
+ RETURNING uuid, created_at, updated_at`
+
+ return p.conn.QueryRow(
+ ctx,
+ query,
+ c.UUID,
+ c.Name,
+ c.Status,
+ c.Priority,
+ c.LandSize,
+ c.GrowthStage,
+ c.PlantID,
+ c.FarmID,
+ ).Scan(&c.CreatedAt, &c.UpdatedAt)
+}
+
+func (p *postgresCroplandRepository) Delete(ctx context.Context, uuid string) error {
+ query := `DELETE FROM croplands WHERE uuid = $1`
+ _, err := p.conn.Exec(ctx, query, uuid)
+ return err
+}
diff --git a/backend/internal/repository/postgres_farm.go b/backend/internal/repository/postgres_farm.go
new file mode 100644
index 0000000..e0fa6a4
--- /dev/null
+++ b/backend/internal/repository/postgres_farm.go
@@ -0,0 +1,102 @@
+package repository
+
+import (
+ "context"
+ "strings"
+
+ "github.com/google/uuid"
+
+ "github.com/forfarm/backend/internal/domain"
+)
+
+type postgresFarmRepository struct {
+ conn Connection
+}
+
+func NewPostgresFarm(conn Connection) domain.FarmRepository {
+ return &postgresFarmRepository{conn: conn}
+}
+
+func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.Farm, error) {
+ rows, err := p.conn.Query(ctx, query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var farms []domain.Farm
+ for rows.Next() {
+ var f domain.Farm
+ if err := rows.Scan(
+ &f.UUID,
+ &f.Name,
+ &f.Lat,
+ &f.Lon,
+ &f.CreatedAt,
+ &f.UpdatedAt,
+ &f.OwnerID,
+ ); err != nil {
+ return nil, err
+ }
+ farms = append(farms, f)
+ }
+ return farms, nil
+}
+
+func (p *postgresFarmRepository) GetByID(ctx context.Context, uuid string) (domain.Farm, error) {
+ query := `
+ SELECT uuid, name, lat, lon, created_at, updated_at, owner_id
+ FROM farms
+ WHERE uuid = $1`
+
+ farms, err := p.fetch(ctx, query, uuid)
+ if err != nil {
+ return domain.Farm{}, err
+ }
+ if len(farms) == 0 {
+ return domain.Farm{}, domain.ErrNotFound
+ }
+ return farms[0], nil
+}
+
+func (p *postgresFarmRepository) GetByOwnerID(ctx context.Context, ownerID string) ([]domain.Farm, error) {
+ query := `
+ SELECT uuid, name, lat, lon, created_at, updated_at, owner_id
+ FROM farms
+ WHERE owner_id = $1`
+
+ return p.fetch(ctx, query, ownerID)
+}
+
+func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.Farm) error {
+ if strings.TrimSpace(f.UUID) == "" {
+ f.UUID = uuid.New().String()
+ }
+
+ query := `
+ INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id)
+ VALUES ($1, $2, $3, $4, NOW(), NOW(), $5)
+ ON CONFLICT (uuid) DO UPDATE
+ SET name = EXCLUDED.name,
+ lat = EXCLUDED.lat,
+ lon = EXCLUDED.lon,
+ updated_at = NOW(),
+ owner_id = EXCLUDED.owner_id
+ RETURNING uuid, created_at, updated_at`
+
+ return p.conn.QueryRow(
+ ctx,
+ query,
+ f.UUID,
+ f.Name,
+ f.Lat,
+ f.Lon,
+ f.OwnerID,
+ ).Scan(&f.CreatedAt, &f.UpdatedAt)
+}
+
+func (p *postgresFarmRepository) Delete(ctx context.Context, uuid string) error {
+ query := `DELETE FROM farms WHERE uuid = $1`
+ _, err := p.conn.Exec(ctx, query, uuid)
+ return err
+}
diff --git a/backend/internal/repository/postgres_user.go b/backend/internal/repository/postgres_user.go
index 6619e96..67a00f5 100644
--- a/backend/internal/repository/postgres_user.go
+++ b/backend/internal/repository/postgres_user.go
@@ -78,15 +78,31 @@ func (p *postgresUserRepository) GetByUsername(ctx context.Context, username str
return users[0], nil
}
-func (p *postgresUserRepository) CreateOrUpdate(ctx context.Context, u *domain.User) error {
- if err := u.Validate(); err != nil {
- return err
- }
+func (p *postgresUserRepository) GetByEmail(ctx context.Context, email string) (domain.User, error) {
+ query := `
+ SELECT id, uuid, username, password, email, created_at, updated_at, is_active
+ FROM users
+ WHERE email = $1`
+ users, err := p.fetch(ctx, query, email)
+ if err != nil {
+ return domain.User{}, err
+ }
+ if len(users) == 0 {
+ return domain.User{}, domain.ErrNotFound
+ }
+ return users[0], nil
+}
+
+func (p *postgresUserRepository) CreateOrUpdate(ctx context.Context, u *domain.User) error {
if strings.TrimSpace(u.UUID) == "" {
u.UUID = uuid.New().String()
}
+ if err := u.Validate(); err != nil {
+ return err
+ }
+
u.NormalizedUsername()
query := `
diff --git a/backend/internal/utilities/jwt.go b/backend/internal/utilities/jwt.go
new file mode 100644
index 0000000..95fbbb0
--- /dev/null
+++ b/backend/internal/utilities/jwt.go
@@ -0,0 +1,45 @@
+package utilities
+
+import (
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// TODO: Change later
+var secretKey = []byte("secret-key")
+
+func CreateJwtToken(uuid string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "uuid": uuid,
+ "exp": time.Now().Add(time.Hour * 24).Unix(),
+ })
+
+ tokenString, err := token.SignedString(secretKey)
+ if err != nil {
+ return "", err
+ }
+
+ return tokenString, nil
+}
+
+func VerifyJwtToken(tokenString string) error {
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, jwt.ErrSignatureInvalid
+ }
+
+ // TODO: CHANGE SECRET KEY
+ return secretKey, nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if !token.Valid {
+ return jwt.ErrSignatureInvalid
+ }
+
+ return nil
+}
diff --git a/backend/migrations/00001_create_user_table.sql b/backend/migrations/00001_create_user_table.sql
index 8b0b6d7..afa1499 100644
--- a/backend/migrations/00001_create_user_table.sql
+++ b/backend/migrations/00001_create_user_table.sql
@@ -2,7 +2,7 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
uuid UUID NOT NULL,
- username TEXT NOT NULL,
+ username TEXT NULL,
password TEXT NOT NULL,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
diff --git a/backend/migrations/00002_create_farm_and_cropland_tables.sql b/backend/migrations/00002_create_farm_and_cropland_tables.sql
new file mode 100644
index 0000000..8f2df4b
--- /dev/null
+++ b/backend/migrations/00002_create_farm_and_cropland_tables.sql
@@ -0,0 +1,67 @@
+-- +goose Up
+CREATE TABLE light_profiles (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE soil_conditions (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE harvest_units (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE plants (
+ uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ variety TEXT,
+ row_spacing DOUBLE PRECISION,
+ optimal_temp DOUBLE PRECISION,
+ planting_depth DOUBLE PRECISION,
+ average_height DOUBLE PRECISION,
+ light_profile_id INT NOT NULL,
+ soil_condition_id INT NOT NULL,
+ planting_detail TEXT,
+ is_perennial BOOLEAN NOT NULL DEFAULT FALSE,
+ days_to_emerge INT,
+ days_to_flower INT,
+ days_to_maturity INT,
+ harvest_window INT,
+ ph_value DOUBLE PRECISION,
+ estimate_loss_rate DOUBLE PRECISION,
+ estimate_revenue_per_hu DOUBLE PRECISION,
+ harvest_unit_id INT NOT NULL,
+ water_needs DOUBLE PRECISION,
+ CONSTRAINT fk_plant_light_profile FOREIGN KEY (light_profile_id) REFERENCES light_profiles(id),
+ CONSTRAINT fk_plant_soil_condition FOREIGN KEY (soil_condition_id) REFERENCES soil_conditions(id),
+ CONSTRAINT fk_plant_harvest_unit FOREIGN KEY (harvest_unit_id) REFERENCES harvest_units(id)
+);
+
+CREATE TABLE farms (
+ uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ lat DOUBLE PRECISION NOT NULL,
+ lon DOUBLE PRECISION NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ owner_id UUID NOT NULL,
+ CONSTRAINT fk_farm_owner FOREIGN KEY (owner_id) REFERENCES users(uuid) ON DELETE CASCADE
+);
+
+CREATE TABLE croplands (
+ uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ status TEXT NOT NULL,
+ priority INT NOT NULL,
+ land_size DOUBLE PRECISION NOT NULL,
+ growth_stage TEXT NOT NULL,
+ plant_id UUID NOT NULL,
+ farm_id UUID NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT fk_cropland_farm FOREIGN KEY (farm_id) REFERENCES farms(uuid) ON DELETE CASCADE,
+ CONSTRAINT fk_cropland_plant FOREIGN KEY (plant_id) REFERENCES plants(uuid) ON DELETE CASCADE
+);
diff --git a/frontend/app/auth/signin/forgot-password-modal.tsx b/frontend/app/auth/signin/forgot-password-modal.tsx
new file mode 100644
index 0000000..c2eb19c
--- /dev/null
+++ b/frontend/app/auth/signin/forgot-password-modal.tsx
@@ -0,0 +1,49 @@
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import { Label } from "@/components/ui/label";
+
+export default function ForgotPasswordModal() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/auth/signin/page.tsx b/frontend/app/auth/signin/page.tsx
new file mode 100644
index 0000000..55dc3dd
--- /dev/null
+++ b/frontend/app/auth/signin/page.tsx
@@ -0,0 +1,76 @@
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Button } from "@/components/ui/button";
+import ForgotPasswordModal from "./forgot-password-modal";
+
+import Link from "next/link";
+import Image from "next/image";
+
+export default function SigninPage() {
+ return (
+
+
+
+
+
+ {/* login box */}
+
+
+
+
+
+
Welcome back.
+
+ New to Forfarm?
+
+
+ Sign up
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Or log in with
+ {/* OAUTH */}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/auth/signin/waterdrop.tsx b/frontend/app/auth/signin/waterdrop.tsx
new file mode 100644
index 0000000..b06bb2f
--- /dev/null
+++ b/frontend/app/auth/signin/waterdrop.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+const WaterDrop = () => {
+ return (
+
+ {/* Water Drop animation */}
+
+
+ );
+};
+
+export default WaterDrop;
diff --git a/frontend/app/auth/signup/page.tsx b/frontend/app/auth/signup/page.tsx
new file mode 100644
index 0000000..5657830
--- /dev/null
+++ b/frontend/app/auth/signup/page.tsx
@@ -0,0 +1,69 @@
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Button } from "@/components/ui/button";
+
+import Link from "next/link";
+import Image from "next/image";
+
+export default function SignupPage() {
+ return (
+
+
+
+
+
+ {/* login box */}
+
+
+
+
+
+
Hi! Welcome
+
+ Already have accounts?
+
+
+ Sign in
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Or log in with
+ {/* OAUTH */}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 4fd7492..491435b 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -33,6 +33,14 @@ body {
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -60,6 +68,14 @@ body {
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index f7fa87e..86c19f8 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,20 +1,28 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Open_Sans, Roboto_Mono } from "next/font/google";
import "./globals.css";
+import { ThemeProvider } from "@/components/theme-provider";
-const geistSans = Geist({
- variable: "--font-geist-sans",
+const openSans = Open_Sans({
subsets: ["latin"],
+ display: "swap",
+ variable: "--font-opensans",
});
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const robotoMono = Roboto_Mono({
subsets: ["latin"],
+ display: "swap",
+ variable: "--font-roboto-mono",
});
+// const geistMono = Geist_Mono({
+// variable: "--font-geist-mono",
+// subsets: ["latin"],
+// });
+
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "ForFarm - Smart Farming Solutions",
+ description: "Optimize your agricultural business with AI-driven insights and real-time data.",
};
export default function RootLayout({
@@ -23,11 +31,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
- {children}
+
+
+
+
+
);
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 9007252..1d68f3c 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -1,100 +1,68 @@
import Image from "next/image";
+import Link from "next/link";
+import { ArrowRight, Cloud, BarChart, Zap } from "lucide-react";
+import { Leaf } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
export default function Home() {
return (
-
-
-
-
- -
- Get started by editing{" "}
-
- app/page.tsx
-
- .
-
- - Save and see your changes instantly.
-
+
+
+
+
+
+ ForFarm
+
+
+
+
+ Documentation
+
+
+ Get started
+
+
+
-
-
-
- Deploy now
-
-
- Read our docs
-
+
+
+
+
Your Smart Farming Platform
+
+ It's a smart and easy way to optimize your agricultural business, with the help of AI-driven insights and
+ real-time data.
+
+
+
+
-
);
diff --git a/frontend/app/setup/google-map-with-drawing.tsx b/frontend/app/setup/google-map-with-drawing.tsx
new file mode 100644
index 0000000..46ddbd5
--- /dev/null
+++ b/frontend/app/setup/google-map-with-drawing.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { GoogleMap, LoadScript, DrawingManager } from "@react-google-maps/api";
+import { useState, useCallback } from "react";
+
+const containerStyle = {
+ width: "100%",
+ height: "500px",
+};
+
+const center = { lat: 13.7563, lng: 100.5018 }; // Example: Bangkok, Thailand
+
+const GoogleMapWithDrawing = () => {
+ const [map, setMap] = useState
(null);
+
+ // Handles drawing complete
+ const onDrawingComplete = useCallback(
+ (overlay: google.maps.drawing.OverlayCompleteEvent) => {
+ console.log("Drawing complete:", overlay);
+ },
+ []
+ );
+
+ return (
+
+ setMap(map)}
+ >
+ {map && (
+
+ )}
+
+
+ );
+};
+
+export default GoogleMapWithDrawing;
diff --git a/frontend/app/setup/harvest-detail-form.tsx b/frontend/app/setup/harvest-detail-form.tsx
new file mode 100644
index 0000000..38e2124
--- /dev/null
+++ b/frontend/app/setup/harvest-detail-form.tsx
@@ -0,0 +1,230 @@
+"use client";
+
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { harvestDetailsFormSchema } from "@/schemas/application.schema";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+type harvestSchema = z.infer;
+
+export default function HarvestDetailsForm() {
+ const form = useForm({
+ resolver: zodResolver(harvestDetailsFormSchema),
+ defaultValues: {},
+ });
+ return (
+
+
+ );
+}
diff --git a/frontend/app/setup/layout.tsx b/frontend/app/setup/layout.tsx
new file mode 100644
index 0000000..393f507
--- /dev/null
+++ b/frontend/app/setup/layout.tsx
@@ -0,0 +1,43 @@
+import { AppSidebar } from "@/components/app-sidebar";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import { Separator } from "@/components/ui/separator";
+import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
+
+export default function AppLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/frontend/app/setup/page.tsx b/frontend/app/setup/page.tsx
new file mode 100644
index 0000000..223ec01
--- /dev/null
+++ b/frontend/app/setup/page.tsx
@@ -0,0 +1,34 @@
+import PlantingDetailsForm from "./planting-detail-form";
+import HarvestDetailsForm from "./harvest-detail-form";
+import { Separator } from "@/components/ui/separator";
+import GoogleMapWithDrawing from "./google-map-with-drawing";
+
+export default function SetupPage() {
+ return (
+
+
+
Plating Details
+
+
+
+
+
Harvest Details
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/setup/planting-detail-form.tsx b/frontend/app/setup/planting-detail-form.tsx
new file mode 100644
index 0000000..e6cc1c2
--- /dev/null
+++ b/frontend/app/setup/planting-detail-form.tsx
@@ -0,0 +1,314 @@
+"use client";
+
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { plantingDetailsFormSchema } from "@/schemas/application.schema";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Textarea } from "@/components/ui/textarea";
+import { Switch } from "@/components/ui/switch";
+
+type plantingSchema = z.infer;
+
+export default function PlantingDetailsForm() {
+ const form = useForm({
+ resolver: zodResolver(plantingDetailsFormSchema),
+ defaultValues: {},
+ });
+ return (
+
+
+ );
+}
diff --git a/frontend/app/signin/page.tsx b/frontend/app/signin/page.tsx
deleted file mode 100644
index 718aa00..0000000
--- a/frontend/app/signin/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-export default function Signin() {
- return (
-
-
-
-
- {/* login box */}
-
-
Login
-
-
-
-
- );
-}
diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx
new file mode 100644
index 0000000..73989cb
--- /dev/null
+++ b/frontend/components/app-sidebar.tsx
@@ -0,0 +1,144 @@
+"use client";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar";
+import {
+ Calendar,
+ Home,
+ Settings,
+ Sun,
+ Moon,
+ LogOut,
+ Wrench,
+ FileText,
+ Bot,
+ Factory,
+ Store,
+} from "lucide-react";
+import { usePathname } from "next/navigation";
+import { useState } from "react";
+import Image from "next/image";
+
+export function AppSidebar() {
+ const pathname = usePathname();
+ const [darkMode, setDarkMode] = useState(false);
+ const items = [
+ {
+ title: "Dashboard",
+ url: "#",
+ icon: Home,
+ },
+ {
+ title: "SetUp",
+ url: "#",
+ icon: Wrench,
+ },
+ {
+ title: "Management",
+ url: "#",
+ icon: Calendar,
+ },
+ {
+ title: "Work Order Management",
+ url: "#",
+ icon: FileText,
+ },
+ {
+ title: "AI-Chatbot",
+ url: "#",
+ icon: Bot,
+ },
+ {
+ title: "Inventory Management",
+ url: "#",
+ icon: Factory,
+ },
+ {
+ title: "Marketplace",
+ url: "#",
+ icon: Store,
+ },
+ {
+ title: "Settings",
+ url: "#",
+ icon: Settings,
+ },
+ ];
+
+ return (
+
+ {/* Menu Items */}
+
+
+
+
+
+
+
ForFarm
+
+
+
+
+
+ {items.map((item) => {
+ const isActive = pathname === item.url;
+
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+
+
+ {/* Bottom Section: Theme Toggle & Logout */}
+
+ {/* Theme Toggle */}
+
+
+ {/* Logout Button */}
+
+
+
+ );
+}
diff --git a/frontend/components/custom-tooltips.tsx b/frontend/components/custom-tooltips.tsx
new file mode 100644
index 0000000..ba3e399
--- /dev/null
+++ b/frontend/components/custom-tooltips.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+ TooltipProvider,
+} from "./ui/tooltip";
+
+interface CustomTooltipProps {
+ message: string;
+ children: React.ReactNode;
+}
+
+const CustomTooltip: React.FC = ({ message, children }) => {
+ return (
+
+
+ {children}
+
+ {message}
+
+
+
+ );
+};
+
+export default CustomTooltip;
diff --git a/frontend/components/navigation-bar/authenticated-top-nav.tsx b/frontend/components/navigation-bar/authenticated-top-nav.tsx
new file mode 100644
index 0000000..767a253
--- /dev/null
+++ b/frontend/components/navigation-bar/authenticated-top-nav.tsx
@@ -0,0 +1,145 @@
+"use client";
+
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import {
+ Bell,
+ Heart,
+ Wallet,
+ ChartPie,
+ CalendarClock,
+ Calendar,
+} from "lucide-react";
+// import { LogoutButton } from "@/components/auth/logoutButton";
+// import { useUserRole } from "@/hooks/useUserRole";
+import CustomTooltip from "../custom-tooltips";
+
+interface AuthenticatedComponentsProps {
+ uid: string;
+ avatarUrl?: string | null;
+ notificationCount: number;
+}
+
+export const AuthenticatedComponents = ({
+ uid,
+ avatarUrl,
+ notificationCount,
+}: AuthenticatedComponentsProps) => {
+ // const { data } = useUserRole();
+
+ // const businessClass =
+ // data?.role === "business"
+ // ? "border-2 border-[#FFD700] bg-[#FFF8DC] dark:bg-[#4B3E2B] rounded-md p-1"
+ // : "";
+
+ return (
+ //
+
+
+
+
+
+ {notificationCount >= 1 && (
+
+
+
+ {notificationCount}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* {data?.role === "investor" && (
+
+
+
+
+
+
+
+ )} */}
+
+
+
+
+
+ {/*chart pie icon for bussiness's dashboard */}
+ {/* {data?.role === "business" && (
+
+
+
+
+
+
+
+ )} */}
+
+
+
+
+
+
+ Profile
+
+
+ {/* {data?.role === "admin" && (
+
+ Admin
+
+ )}
+ {data?.role === "business" && (
+ <>
+
+ Calendar
+
+
+ >
+ )} */}
+ {/* {data != null && data != undefined && data.role === "business" && (
+
+ Dataroom
+
+ )} */}
+
+ {/* */}
+
+
+
+ );
+};
diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx
new file mode 100644
index 0000000..2e0ad9b
--- /dev/null
+++ b/frontend/components/theme-provider.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+
+type ThemeProviderProps = React.ComponentProps;
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children};
+}
diff --git a/frontend/components/theme-toggle.tsx b/frontend/components/theme-toggle.tsx
new file mode 100644
index 0000000..3daf624
--- /dev/null
+++ b/frontend/components/theme-toggle.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import * as React from "react";
+import { Moon, Sun } from "lucide-react";
+import { useTheme } from "next-themes";
+
+import { Button } from "@/components/ui/button";
+
+export function ThemeToggle() {
+ const { setTheme, theme } = useTheme();
+
+ return (
+
+ );
+}
diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/frontend/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/frontend/components/ui/breadcrumb.tsx b/frontend/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/frontend/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
new file mode 100644
index 0000000..65d4fcd
--- /dev/null
+++ b/frontend/components/ui/button.tsx
@@ -0,0 +1,57 @@
+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 buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx
new file mode 100644
index 0000000..c6fdd07
--- /dev/null
+++ b/frontend/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx
new file mode 100644
index 0000000..1647513
--- /dev/null
+++ b/frontend/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..082639f
--- /dev/null
+++ b/frontend/components/ui/dropdown-menu.tsx
@@ -0,0 +1,201 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/frontend/components/ui/form.tsx b/frontend/components/ui/form.tsx
new file mode 100644
index 0000000..b6daa65
--- /dev/null
+++ b/frontend/components/ui/form.tsx
@@ -0,0 +1,178 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx
new file mode 100644
index 0000000..69b64fb
--- /dev/null
+++ b/frontend/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx
new file mode 100644
index 0000000..5341821
--- /dev/null
+++ b/frontend/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx
new file mode 100644
index 0000000..0cbf77d
--- /dev/null
+++ b/frontend/components/ui/select.tsx
@@ -0,0 +1,159 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/frontend/components/ui/separator.tsx b/frontend/components/ui/separator.tsx
new file mode 100644
index 0000000..12d81c4
--- /dev/null
+++ b/frontend/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/frontend/components/ui/sheet.tsx b/frontend/components/ui/sheet.tsx
new file mode 100644
index 0000000..272cb72
--- /dev/null
+++ b/frontend/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+
+
+ Close
+
+ {children}
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/frontend/components/ui/sidebar.tsx b/frontend/components/ui/sidebar.tsx
new file mode 100644
index 0000000..5e9f37b
--- /dev/null
+++ b/frontend/components/ui/sidebar.tsx
@@ -0,0 +1,763 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { PanelLeft } 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 } 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 SidebarContext = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ 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(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+ }
+)
+SidebarProvider.displayName = "SidebarProvider"
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+ }
+)
+Sidebar.displayName = "Sidebar"
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarTrigger.displayName = "SidebarTrigger"
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarRail.displayName = "SidebarRail"
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInset.displayName = "SidebarInset"
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInput.displayName = "SidebarInput"
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarHeader.displayName = "SidebarHeader"
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarFooter.displayName = "SidebarFooter"
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarSeparator.displayName = "SidebarSeparator"
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarContent.displayName = "SidebarContent"
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarGroup.displayName = "SidebarGroup"
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupLabel.displayName = "SidebarGroupLabel"
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupAction.displayName = "SidebarGroupAction"
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarGroupContent.displayName = "SidebarGroupContent"
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenu.displayName = "SidebarMenu"
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuItem.displayName = "SidebarMenuItem"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none 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",
+ },
+ }
+)
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+ }
+)
+SidebarMenuButton.displayName = "SidebarMenuButton"
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md: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 &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuAction.displayName = "SidebarMenuAction"
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuBadge.displayName = "SidebarMenuBadge"
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+})
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuSub.displayName = "SidebarMenuSub"
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => )
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "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}
+ />
+ )
+})
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
+
+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,
+}
diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx
new file mode 100644
index 0000000..d7e45f7
--- /dev/null
+++ b/frontend/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/frontend/components/ui/switch.tsx b/frontend/components/ui/switch.tsx
new file mode 100644
index 0000000..5f4117f
--- /dev/null
+++ b/frontend/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/frontend/components/ui/textarea.tsx b/frontend/components/ui/textarea.tsx
new file mode 100644
index 0000000..e56b0af
--- /dev/null
+++ b/frontend/components/ui/textarea.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef<
+ HTMLTextAreaElement,
+ React.ComponentProps<"textarea">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/frontend/components/ui/tooltip.tsx b/frontend/components/ui/tooltip.tsx
new file mode 100644
index 0000000..a66b3f2
--- /dev/null
+++ b/frontend/components/ui/tooltip.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/frontend/hooks/use-mobile.tsx b/frontend/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/frontend/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(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
+}
diff --git a/frontend/package.json b/frontend/package.json
index 9ac9920..939ee9c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,15 +9,31 @@
"lint": "next lint"
},
"dependencies": {
+ "@hookform/resolvers": "^4.0.0",
+ "@radix-ui/react-avatar": "^1.1.3",
+ "@radix-ui/react-checkbox": "^1.1.4",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
+ "@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-select": "^2.1.6",
+ "@radix-ui/react-separator": "^1.1.2",
+ "@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-switch": "^1.1.3",
+ "@radix-ui/react-tooltip": "^1.1.8",
+ "@react-google-maps/api": "^2.20.6",
"@tailwindcss/typography": "^0.5.16",
+ "@tanstack/react-query": "^5.66.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.475.0",
"next": "15.1.0",
+ "next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.2",
"tailwind-merge": "^3.0.1",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index f866b41..b2bd51c 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -5,9 +5,48 @@ settings:
excludeLinksFromLockfile: false
dependencies:
+ '@hookform/resolvers':
+ specifier: ^4.0.0
+ version: 4.0.0(react-hook-form@7.54.2)
+ '@radix-ui/react-avatar':
+ specifier: ^1.1.3
+ version: 1.1.3(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-checkbox':
+ specifier: ^1.1.4
+ version: 1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-dialog':
+ specifier: ^1.1.6
+ version: 1.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.6
+ version: 2.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-label':
+ specifier: ^2.1.2
+ version: 2.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-select':
+ specifier: ^2.1.6
+ version: 2.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.2
+ version: 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-slot':
+ specifier: ^1.1.2
+ version: 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-switch':
+ specifier: ^1.1.3
+ version: 1.1.3(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.1.8
+ version: 1.1.8(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@react-google-maps/api':
+ specifier: ^2.20.6
+ version: 2.20.6(react-dom@19.0.0)(react@19.0.0)
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@3.4.17)
+ '@tanstack/react-query':
+ specifier: ^5.66.0
+ version: 5.66.0(react@19.0.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -20,18 +59,27 @@ dependencies:
next:
specifier: 15.1.0
version: 15.1.0(react-dom@19.0.0)(react@19.0.0)
+ next-themes:
+ specifier: ^0.4.4
+ version: 0.4.4(react-dom@19.0.0)(react@19.0.0)
react:
specifier: ^19.0.0
version: 19.0.0
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
+ react-hook-form:
+ specifier: ^7.54.2
+ version: 7.54.2(react@19.0.0)
tailwind-merge:
specifier: ^3.0.1
version: 3.0.1
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17)
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.2
devDependencies:
'@eslint/eslintrc':
@@ -151,6 +199,53 @@ packages:
levn: 0.4.1
dev: true
+ /@floating-ui/core@1.6.9:
+ resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
+ dependencies:
+ '@floating-ui/utils': 0.2.9
+ dev: false
+
+ /@floating-ui/dom@1.6.13:
+ resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
+ dependencies:
+ '@floating-ui/core': 1.6.9
+ '@floating-ui/utils': 0.2.9
+ dev: false
+
+ /@floating-ui/react-dom@2.1.2(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+ dependencies:
+ '@floating-ui/dom': 1.6.13
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@floating-ui/utils@0.2.9:
+ resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ dev: false
+
+ /@googlemaps/js-api-loader@1.16.8:
+ resolution: {integrity: sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==}
+ dev: false
+
+ /@googlemaps/markerclusterer@2.5.3:
+ resolution: {integrity: sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ supercluster: 8.0.1
+ dev: false
+
+ /@hookform/resolvers@4.0.0(react-hook-form@7.54.2):
+ resolution: {integrity: sha512-93ZueVlTaeMF0pRbrLbcnzrxeb2mGA/xyO3RgfrsKs5OCtcfjoWcdjBJm+N7096Jfg/JYlGPjuyQCgqVEodSTg==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+ dependencies:
+ react-hook-form: 7.54.2(react@19.0.0)
+ dev: false
+
/@humanfs/core@0.19.1:
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -506,6 +601,728 @@ packages:
requiresBuild: true
optional: true
+ /@radix-ui/number@1.1.0:
+ resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
+ dev: false
+
+ /@radix-ui/primitive@1.1.1:
+ resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
+ dev: false
+
+ /@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-checkbox@1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-slot': 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-context@1.1.1(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-slot': 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ aria-hidden: 1.2.4
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-remove-scroll: 2.6.3(@types/react@19.0.8)(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-direction@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-menu': 2.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-id@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-label@2.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-menu@2.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-slot': 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ aria-hidden: 1.2.4
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-remove-scroll: 2.6.3(@types/react@19.0.8)(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-arrow': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/rect': 1.1.0
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-slot': 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-select@2.1.6(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-slot': 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ aria-hidden: 1.2.4
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-remove-scroll: 2.6.3(@types/react@19.0.8)(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-slot@1.1.2(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-switch@1.1.3(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@radix-ui/react-slot': 1.1.2(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-use-previous@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-use-rect@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/rect': 1.1.0
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-use-size@1.1.0(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@types/react': 19.0.8
+ react: 19.0.0
+ dev: false
+
+ /@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3)(@types/react@19.0.8)(react-dom@19.0.0)(react@19.0.0)
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@radix-ui/rect@1.1.0:
+ resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
+ dev: false
+
+ /@react-google-maps/api@2.20.6(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-frxkSHWbd36ayyxrEVopSCDSgJUT1tVKXvQld2IyzU3UnDuqqNA3AZE4/fCdqQb2/zBQx3nrWnZB1wBXDcrjcw==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19
+ react-dom: ^16.8 || ^17 || ^18 || ^19
+ dependencies:
+ '@googlemaps/js-api-loader': 1.16.8
+ '@googlemaps/markerclusterer': 2.5.3
+ '@react-google-maps/infobox': 2.20.0
+ '@react-google-maps/marker-clusterer': 2.20.0
+ '@types/google.maps': 3.58.1
+ invariant: 2.2.4
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
+ /@react-google-maps/infobox@2.20.0:
+ resolution: {integrity: sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==}
+ dev: false
+
+ /@react-google-maps/marker-clusterer@2.20.0:
+ resolution: {integrity: sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==}
+ dev: false
+
/@rtsao/scc@1.1.0:
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
dev: true
@@ -536,10 +1353,27 @@ packages:
tailwindcss: 3.4.17
dev: false
+ /@tanstack/query-core@5.66.0:
+ resolution: {integrity: sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==}
+ dev: false
+
+ /@tanstack/react-query@5.66.0(react@19.0.0):
+ resolution: {integrity: sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==}
+ peerDependencies:
+ react: ^18 || ^19
+ dependencies:
+ '@tanstack/query-core': 5.66.0
+ react: 19.0.0
+ dev: false
+
/@types/estree@1.0.6:
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
dev: true
+ /@types/google.maps@3.58.1:
+ resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==}
+ dev: false
+
/@types/json-schema@7.0.15:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
@@ -560,13 +1394,11 @@ packages:
'@types/react': ^19.0.0
dependencies:
'@types/react': 19.0.8
- dev: true
/@types/react@19.0.8:
resolution: {integrity: sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==}
dependencies:
csstype: 3.1.3
- dev: true
/@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0)(eslint@9.20.1)(typescript@5.7.3):
resolution: {integrity: sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==}
@@ -742,6 +1574,13 @@ packages:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
+ /aria-hidden@1.2.4:
+ resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
+ engines: {node: '>=10'}
+ dependencies:
+ tslib: 2.8.1
+ dev: false
+
/aria-query@5.3.2:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
@@ -1020,7 +1859,6 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- dev: true
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -1105,6 +1943,10 @@ packages:
dev: false
optional: true
+ /detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ dev: false
+
/didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -1563,7 +2405,6 @@ packages:
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
- dev: true
/fast-glob@3.3.1:
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
@@ -1688,6 +2529,11 @@ packages:
math-intrinsics: 1.1.0
dev: true
+ /get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+ dev: false
+
/get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
@@ -1828,6 +2674,12 @@ packages:
side-channel: 1.1.0
dev: true
+ /invariant@2.2.4:
+ resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
/is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -2055,7 +2907,6 @@ packages:
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- dev: true
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
@@ -2093,6 +2944,10 @@ packages:
object.values: 1.2.1
dev: true
+ /kdbush@4.0.2:
+ resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
+ dev: false
+
/keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies:
@@ -2148,7 +3003,6 @@ packages:
hasBin: true
dependencies:
js-tokens: 4.0.0
- dev: true
/lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -2217,6 +3071,16 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
+ /next-themes@0.4.4(react-dom@19.0.0)(react@19.0.0):
+ resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ dependencies:
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ dev: false
+
/next@15.1.0(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@@ -2528,10 +3392,70 @@ packages:
scheduler: 0.25.0
dev: false
+ /react-hook-form@7.54.2(react@19.0.0):
+ resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+ dependencies:
+ react: 19.0.0
+ dev: false
+
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
+ /react-remove-scroll-bar@2.3.8(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ react-style-singleton: 2.2.3(@types/react@19.0.8)(react@19.0.0)
+ tslib: 2.8.1
+ dev: false
+
+ /react-remove-scroll@2.6.3(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ react-remove-scroll-bar: 2.3.8(@types/react@19.0.8)(react@19.0.0)
+ react-style-singleton: 2.2.3(@types/react@19.0.8)(react@19.0.0)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.0.8)(react@19.0.0)
+ use-sidecar: 1.1.3(@types/react@19.0.8)(react@19.0.0)
+ dev: false
+
+ /react-style-singleton@2.2.3(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ get-nonce: 1.0.1
+ react: 19.0.0
+ tslib: 2.8.1
+ dev: false
+
/react@19.0.0:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
@@ -2924,6 +3848,12 @@ packages:
pirates: 4.0.6
ts-interface-checker: 0.1.13
+ /supercluster@8.0.1:
+ resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
+ dependencies:
+ kdbush: 4.0.2
+ dev: false
+
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -3102,6 +4032,37 @@ packages:
punycode: 2.3.1
dev: true
+ /use-callback-ref@1.3.3(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ react: 19.0.0
+ tslib: 2.8.1
+ dev: false
+
+ /use-sidecar@1.1.3(@types/react@19.0.8)(react@19.0.0):
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 19.0.8
+ detect-node-es: 1.1.0
+ react: 19.0.0
+ tslib: 2.8.1
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -3194,3 +4155,7 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
+
+ /zod@3.24.2:
+ resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
+ dev: false
diff --git a/frontend/public/forfarm-logo.png b/frontend/public/forfarm-logo.png
new file mode 100644
index 0000000..055bc3e
Binary files /dev/null and b/frontend/public/forfarm-logo.png differ
diff --git a/frontend/public/google-logo.png b/frontend/public/google-logo.png
new file mode 100644
index 0000000..da09768
Binary files /dev/null and b/frontend/public/google-logo.png differ
diff --git a/frontend/public/plant-background.jpeg b/frontend/public/plant-background.jpeg
new file mode 100644
index 0000000..197b360
Binary files /dev/null and b/frontend/public/plant-background.jpeg differ
diff --git a/frontend/public/water-pot.png b/frontend/public/water-pot.png
new file mode 100644
index 0000000..71f4db5
Binary files /dev/null and b/frontend/public/water-pot.png differ
diff --git a/frontend/schemas/application.schema.ts b/frontend/schemas/application.schema.ts
new file mode 100644
index 0000000..d860fd5
--- /dev/null
+++ b/frontend/schemas/application.schema.ts
@@ -0,0 +1,39 @@
+import { z } from "zod";
+
+const plantingDetailsFormSchema = z.object({
+ daysToEmerge: z.number().int().min(0, "Days to emerge must be at least 0"),
+ plantSpacing: z.number().min(0, "Plant spacing must be positive"),
+ rowSpacing: z.number().min(0, "Row spacing must be positive"),
+ plantingDepth: z.number().min(0, "Planting depth must be positive"),
+ averageHeight: z.number().min(0, "Average height must be positive"),
+ startMethod: z.string().optional(),
+ lightProfile: z.string().optional(),
+ soilConditions: z.string().optional(),
+ plantingDetails: z.string().optional(),
+ pruningDetails: z.string().optional(),
+ isPerennial: z.boolean(),
+ autoCreateTasks: z.boolean(),
+});
+
+const harvestDetailsFormSchema = z.object({
+ daysToFlower: z.number().int().min(0, "Days to flower must be at least 0"),
+ daysToMaturity: z
+ .number()
+ .int()
+ .min(0, "Days to maturity must be at least 0"),
+ harvestWindow: z.number().int().min(0, "Harvest window must be at least 0"),
+ estimatedLossRate: z
+ .number()
+ .min(0, "Loss rate must be positive")
+ .max(100, "Loss rate cannot exceed 100"),
+ harvestUnits: z.string().min(1, "Harvest units are required"),
+ estimatedRevenue: z.number().min(0, "Estimated revenue must be positive"),
+ expectedYieldPer100ft: z
+ .number()
+ .min(0, "Expected yield per 100ft must be positive"),
+ expectedYieldPerAcre: z
+ .number()
+ .min(0, "Expected yield per acre must be positive"),
+});
+
+export { plantingDetailsFormSchema, harvestDetailsFormSchema };
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index 18e8cd5..3cc13e4 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -9,6 +9,10 @@ export default {
],
theme: {
extend: {
+ fontFamily: {
+ sans: ["var(--font-opensans)"],
+ mono: ["var(--font-roboto-mono)"],
+ },
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
@@ -50,12 +54,41 @@ export default {
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
+ sidebar: {
+ DEFAULT: "hsl(var(--sidebar-background))",
+ foreground: "hsl(var(--sidebar-foreground))",
+ primary: "hsl(var(--sidebar-primary))",
+ "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
+ accent: "hsl(var(--sidebar-accent))",
+ "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
+ border: "hsl(var(--sidebar-border))",
+ ring: "hsl(var(--sidebar-ring))",
+ },
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
+ keyframes: {
+ dropAnimation: {
+ "0%": {
+ transform: "translateY(-50px)",
+ borderRadius: "50%",
+ width: "1.5rem",
+ height: "1.5rem",
+ },
+ "100%": {
+ transform: "translateY(100vh)",
+ borderRadius: "50% 50% 50% 50%",
+ width: "1rem",
+ height: "2rem",
+ },
+ },
+ },
+ animation: {
+ drop: "dropAnimation 2s ease-in-out infinite",
+ },
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],