From a3f46b6b38af5f032f1061ad6cb70fe706a7c5ee Mon Sep 17 00:00:00 2001 From: bernard-ng Date: Wed, 12 Nov 2025 16:51:59 +0200 Subject: [PATCH] feat(dashboard): setting up layout --- apps/api/.env | 3 +- apps/api/package.json | 4 + apps/api/src/config.ts | 1 + apps/api/src/index.ts | 11 + apps/api/src/schemas/sources.ts | 69 ++ apps/api/src/trpc/init.ts | 67 ++ apps/api/src/trpc/middlewares/auth.ts | 36 + apps/api/src/trpc/middlewares/db.ts | 23 + apps/api/src/trpc/routers/_app.ts | 13 + apps/api/src/trpc/routers/sources.ts | 20 + apps/api/src/utils/auth.ts | 40 + apps/dashboard/next.config.ts | 18 +- apps/dashboard/package.json | 17 +- .../(app)/(sidebar)/articles/page.tsx | 12 + .../app/[locale]/(app)/(sidebar)/layout.tsx | 44 ++ .../[locale]/(app)/(sidebar)/sources/page.tsx | 12 + .../src/app/[locale]/(public)/login/page.tsx | 32 + apps/dashboard/src/app/[locale]/error.tsx | 30 + apps/dashboard/src/app/[locale]/layout.tsx | 56 ++ apps/dashboard/src/app/[locale]/not-found.tsx | 13 + apps/dashboard/src/app/[locale]/providers.tsx | 30 + apps/dashboard/src/app/global-error.tsx | 13 + apps/dashboard/src/app/layout.tsx | 34 - apps/dashboard/src/app/page.tsx | 59 -- .../src/components/forms/login-form.tsx | 59 ++ apps/dashboard/src/components/providers.tsx | 18 - .../src/components/sidebar/app-sidebar.tsx | 174 +++++ .../src/components/sidebar/nav-main.tsx | 72 ++ .../src/components/sidebar/nav-projects.tsx | 82 ++ .../src/components/sidebar/nav-user.tsx | 102 +++ .../src/components/sidebar/team-switcher.tsx | 88 +++ .../dashboard/src/hooks/use-calendar-dates.ts | 30 + apps/dashboard/src/hooks/use-local-storage.ts | 69 ++ apps/dashboard/src/hooks/use-zod-form.ts | 14 + apps/dashboard/src/locales/client.ts | 11 + apps/dashboard/src/locales/server.ts | 5 + apps/dashboard/src/locales/translations/en.ts | 3 + apps/dashboard/src/proxy.ts | 50 ++ apps/dashboard/src/trpc/client.tsx | 67 ++ apps/dashboard/src/trpc/query-client.ts | 20 + apps/dashboard/src/trpc/server.tsx | 80 ++ apps/dashboard/src/utils/categories.ts | 101 +++ apps/dashboard/src/utils/environment.ts | 11 + apps/dashboard/tsconfig.json | 9 +- biome.json | 8 +- bun.lock | 82 ++ package.json | 1 + packages/db/drizzle.config.ts | 3 +- packages/db/src/config.ts | 2 +- packages/db/src/queries/sources.ts | 40 +- packages/ui/package.json | 7 +- packages/ui/src/components/avatar.tsx | 40 + packages/ui/src/components/breadcrumb.tsx | 101 +++ packages/ui/src/components/collapsible.tsx | 21 + packages/ui/src/components/dropdown-menu.tsx | 227 ++++++ packages/ui/src/components/sidebar.tsx | 698 ++++++++++++++++++ packages/ui/src/components/skeleton.tsx | 13 + packages/ui/src/components/sonner.tsx | 40 + packages/ui/src/components/tooltip.tsx | 56 ++ packages/ui/src/hooks/use-mobile.ts | 19 + .../ui/src/styles/{global.css => globals.css} | 0 61 files changed, 2957 insertions(+), 123 deletions(-) create mode 100644 apps/api/src/schemas/sources.ts create mode 100644 apps/api/src/trpc/init.ts create mode 100644 apps/api/src/trpc/middlewares/auth.ts create mode 100644 apps/api/src/trpc/middlewares/db.ts create mode 100644 apps/api/src/trpc/routers/_app.ts create mode 100644 apps/api/src/trpc/routers/sources.ts create mode 100644 apps/api/src/utils/auth.ts create mode 100644 apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx create mode 100644 apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx create mode 100644 apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx create mode 100644 apps/dashboard/src/app/[locale]/(public)/login/page.tsx create mode 100644 apps/dashboard/src/app/[locale]/error.tsx create mode 100644 apps/dashboard/src/app/[locale]/layout.tsx create mode 100644 apps/dashboard/src/app/[locale]/not-found.tsx create mode 100644 apps/dashboard/src/app/[locale]/providers.tsx create mode 100644 apps/dashboard/src/app/global-error.tsx delete mode 100644 apps/dashboard/src/app/layout.tsx delete mode 100644 apps/dashboard/src/app/page.tsx create mode 100644 apps/dashboard/src/components/forms/login-form.tsx delete mode 100644 apps/dashboard/src/components/providers.tsx create mode 100644 apps/dashboard/src/components/sidebar/app-sidebar.tsx create mode 100644 apps/dashboard/src/components/sidebar/nav-main.tsx create mode 100644 apps/dashboard/src/components/sidebar/nav-projects.tsx create mode 100644 apps/dashboard/src/components/sidebar/nav-user.tsx create mode 100644 apps/dashboard/src/components/sidebar/team-switcher.tsx create mode 100644 apps/dashboard/src/hooks/use-calendar-dates.ts create mode 100644 apps/dashboard/src/hooks/use-local-storage.ts create mode 100644 apps/dashboard/src/hooks/use-zod-form.ts create mode 100644 apps/dashboard/src/locales/client.ts create mode 100644 apps/dashboard/src/locales/server.ts create mode 100644 apps/dashboard/src/locales/translations/en.ts create mode 100644 apps/dashboard/src/proxy.ts create mode 100644 apps/dashboard/src/trpc/client.tsx create mode 100644 apps/dashboard/src/trpc/query-client.ts create mode 100644 apps/dashboard/src/trpc/server.tsx create mode 100644 apps/dashboard/src/utils/categories.ts create mode 100644 apps/dashboard/src/utils/environment.ts create mode 100644 packages/ui/src/components/avatar.tsx create mode 100644 packages/ui/src/components/breadcrumb.tsx create mode 100644 packages/ui/src/components/collapsible.tsx create mode 100644 packages/ui/src/components/dropdown-menu.tsx create mode 100644 packages/ui/src/components/sidebar.tsx create mode 100644 packages/ui/src/components/skeleton.tsx create mode 100644 packages/ui/src/components/sonner.tsx create mode 100644 packages/ui/src/components/tooltip.tsx create mode 100644 packages/ui/src/hooks/use-mobile.ts rename packages/ui/src/styles/{global.css => globals.css} (100%) diff --git a/apps/api/.env b/apps/api/.env index 6a6bcde..5c75532 100644 --- a/apps/api/.env +++ b/apps/api/.env @@ -1,6 +1,7 @@ NODE_ENV=development BASANGO_API_HOST=localhost -BASANGO_API_PORT=3000 +BASANGO_API_PORT=3080 BASANGO_API_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 BASANGO_API_KEY=your_api_key_here BASANGO_CRAWLER_KEY=dev +BASANGO_JWT_SECRET=your_jwt_secret_here diff --git a/apps/api/package.json b/apps/api/package.json index 8706206..e14d789 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -4,6 +4,7 @@ "@basango/encryption": "workspace:*", "@basango/logger": "workspace:*", "@hono/node-server": "^1.19.6", + "@hono/trpc-server": "^0.4.0", "@hono/zod-openapi": "^1.1.4", "@scalar/hono-api-reference": "^0.9.24", "@trpc/server": "^11.7.1", @@ -16,6 +17,9 @@ "zod": "catalog:", "zod-openapi": "^5.4.3" }, + "exports": { + "./trpc/routers/_app": "./src/trpc/routers/_app.ts" + }, "name": "@basango/api", "private": true, "scripts": { diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 29c709b..520f923 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -28,6 +28,7 @@ export const { env, config } = defineConfig({ "BASANGO_API_ALLOWED_ORIGINS", "BASANGO_API_KEY", "BASANGO_CRAWLER_KEY", + "BASANGO_JWT_SECRET", ], path: path.join(PROJECT_DIR, ".env"), }, diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 9192502..6cc9ac9 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,3 +1,4 @@ +import { trpcServer } from "@hono/trpc-server"; import { OpenAPIHono } from "@hono/zod-openapi"; import { Scalar } from "@scalar/hono-api-reference"; import { cors } from "hono/cors"; @@ -5,6 +6,8 @@ import { secureHeaders } from "hono/secure-headers"; import { config, env } from "@/config"; import { routers } from "@/rest/routers"; +import { createTRPCContext } from "@/trpc/init"; +import { appRouter } from "@/trpc/routers/_app"; const app = new OpenAPIHono(); @@ -21,6 +24,14 @@ app.use( }), ); +app.use( + "/trpc/*", + trpcServer({ + createContext: createTRPCContext, + router: appRouter, + }), +); + app.doc("/openapi", { info: { contact: { diff --git a/apps/api/src/schemas/sources.ts b/apps/api/src/schemas/sources.ts new file mode 100644 index 0000000..abbd355 --- /dev/null +++ b/apps/api/src/schemas/sources.ts @@ -0,0 +1,69 @@ +import { z } from "zod"; + +const idSchema = z.uuid().openapi({ + description: "The unique identifier of the source.", + example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g", +}); + +const biasSchema = z.enum(["neutral", "slightly", "partisan", "extreme"]).openapi({ + description: "The bias level of the source.", + example: "neutral", +}); +const reliabilitySchema = z + .enum(["trusted", "reliable", "average", "low_trust", "unreliable"]) + .openapi({ + description: "The reliability level of the source.", + example: "trusted", + }); + +const transparencySchema = z.enum(["high", "medium", "low"]).openapi({ + description: "The transparency level of the source.", + example: "high", +}); + +const credibilitySchema = z + .object({ + bias: biasSchema.default("neutral"), + reliability: reliabilitySchema.default("average"), + transparency: transparencySchema.default("medium"), + }) + .openapi({ + description: "Credibility information about the source.", + }); + +export const createSourceSchema = z.object({ + credibility: credibilitySchema.optional(), + description: z.string().max(1024).optional().openapi({ + description: "A brief description of the source.", + example: "Radio Okapi is a Congolese radio station that provides news and information.", + }), + displayName: z.string().min(1).max(255).optional().openapi({ + description: "The display name of the source.", + example: "Radio Okapi", + }), + name: z.string().min(1).max(255).openapi({ + description: "The name of the source.", + example: "radiookapi.com", + }), + url: z.url().openapi({ + description: "The URL of the source.", + example: "https://techcrunch.com", + }), +}); + +export const getSourceSchema = z.object({ + id: idSchema, +}); + +export const updateSourceSchema = z.object({ + credibility: credibilitySchema.optional(), + description: createSourceSchema.shape.description, + displayName: createSourceSchema.shape.displayName, + id: idSchema, + name: createSourceSchema.shape.name.optional(), +}); + +export const createSourceResponseSchema = z.object({ + id: idSchema, + ...createSourceSchema.shape, +}); diff --git a/apps/api/src/trpc/init.ts b/apps/api/src/trpc/init.ts new file mode 100644 index 0000000..3858361 --- /dev/null +++ b/apps/api/src/trpc/init.ts @@ -0,0 +1,67 @@ +import { Database, db } from "@basango/db/client"; +import { initTRPC } from "@trpc/server"; +import type { Context } from "hono"; +import superjson from "superjson"; + +import { withAuthentication } from "@/trpc/middlewares/auth"; +import { withDatabase } from "@/trpc/middlewares/db"; +import { Session, verifyAccessToken } from "@/utils/auth"; +import { getGeoContext } from "@/utils/geo"; + +type TRPCContext = { + session: Session | null; + db: Database; + geo: ReturnType; +}; + +export const createTRPCContext = async (_: unknown, c: Context): Promise => { + const accessToken = c.req.header("Authorization")?.split(" ")[1]; + const session = await verifyAccessToken(accessToken); + const geo = getGeoContext(c.req); + + return { + db, + geo, + session, + }; +}; + +const t = initTRPC.context().create({ + transformer: superjson, +}); + +export const createTRPCRouter = t.router; +export const createCallerFactory = t.createCallerFactory; + +const withDatabaseMiddleware = t.middleware(async (opts) => { + return withDatabase({ + ctx: opts.ctx, + next: opts.next, + }); +}); + +const withAutenticationMiddleware = t.middleware(async (opts) => { + return withAuthentication({ + ctx: opts.ctx, + next: opts.next, + }); +}); + +export const publicProcedure = t.procedure.use(withDatabaseMiddleware); + +export const protectedProcedure = t.procedure + .use(withDatabaseMiddleware) + .use(withAutenticationMiddleware) // NOTE: This is needed to ensure that the teamId is set in the context + .use(async (opts) => { + const { session } = opts.ctx; + + // if (!session) { + // throw new TRPCError({ code: "UNAUTHORIZED" }); + // } + + return opts.next({ + ctx: { + session, + }, + }); + }); diff --git a/apps/api/src/trpc/middlewares/auth.ts b/apps/api/src/trpc/middlewares/auth.ts new file mode 100644 index 0000000..45bb854 --- /dev/null +++ b/apps/api/src/trpc/middlewares/auth.ts @@ -0,0 +1,36 @@ +import type { Database } from "@basango/db/client"; + +// import { TRPCError } from "@trpc/server"; + +import type { Session } from "@/utils/auth"; + +export const withAuthentication = async (opts: { + ctx: { + session?: Session | null; + db: Database; + }; + next: (opts: { + ctx: { + session?: Session | null; + db: Database; + }; + }) => Promise; +}) => { + const { ctx, next } = opts; + + // const userId = ctx.session?.user?.id; + + // if (!userId) { + // throw new TRPCError({ + // code: "UNAUTHORIZED", + // message: "No permission to access", + // }); + // } + + return next({ + ctx: { + db: ctx.db, + session: ctx.session, + }, + }); +}; diff --git a/apps/api/src/trpc/middlewares/db.ts b/apps/api/src/trpc/middlewares/db.ts new file mode 100644 index 0000000..0405702 --- /dev/null +++ b/apps/api/src/trpc/middlewares/db.ts @@ -0,0 +1,23 @@ +import { type Database, db } from "@basango/db/client"; + +import type { Session } from "@/utils/auth"; + +export const withDatabase = async (opts: { + ctx: { + session?: Session | null; + db: Database; + }; + next: (opts: { + ctx: { + session?: Session | null; + db: Database; + }; + }) => Promise; +}) => { + const { ctx, next } = opts; + + ctx.db = db; + const result = await next({ ctx }); + + return result; +}; diff --git a/apps/api/src/trpc/routers/_app.ts b/apps/api/src/trpc/routers/_app.ts new file mode 100644 index 0000000..3aa851d --- /dev/null +++ b/apps/api/src/trpc/routers/_app.ts @@ -0,0 +1,13 @@ +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; + +import { createTRPCRouter } from "@/trpc/init"; +import { sourcesRouter } from "@/trpc/routers/sources"; + +export const appRouter = createTRPCRouter({ + sources: sourcesRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; +export type RouterOutputs = inferRouterOutputs; +export type RouterInputs = inferRouterInputs; diff --git a/apps/api/src/trpc/routers/sources.ts b/apps/api/src/trpc/routers/sources.ts new file mode 100644 index 0000000..64bb8e9 --- /dev/null +++ b/apps/api/src/trpc/routers/sources.ts @@ -0,0 +1,20 @@ +import { createSource, getSourceById, getSources, updateSource } from "@basango/db/queries"; + +import { createSourceSchema, getSourceSchema, updateSourceSchema } from "@/schemas/sources"; +import { createTRPCRouter, protectedProcedure } from "@/trpc/init"; + +export const sourcesRouter = createTRPCRouter({ + create: protectedProcedure.input(createSourceSchema).mutation(async ({ ctx, input }) => { + return createSource(ctx.db, { ...input }); + }), + + get: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)), + + getById: protectedProcedure.input(getSourceSchema).query(async ({ ctx, input }) => { + return getSourceById(ctx.db, { ...input }); + }), + + update: protectedProcedure.input(updateSourceSchema).mutation(async ({ ctx, input }) => { + return updateSource(ctx.db, { ...input }); + }), +}); diff --git a/apps/api/src/utils/auth.ts b/apps/api/src/utils/auth.ts new file mode 100644 index 0000000..902b296 --- /dev/null +++ b/apps/api/src/utils/auth.ts @@ -0,0 +1,40 @@ +import { type JWTPayload, jwtVerify } from "jose"; + +import { env } from "@/config"; + +export type Session = { + user: { + id: string; + email: string; + full_name?: string; + }; +}; + +export type VerifiedJWTPayload = JWTPayload & { + user: { + id: string; + email: string; + full_name?: string; + }; +}; + +export async function verifyAccessToken(accessToken?: string): Promise { + if (!accessToken) return null; + + try { + const { payload } = await jwtVerify( + accessToken, + new TextEncoder().encode(env("BASANGO_JWT_SECRET")), + ); + + return { + user: { + email: payload.user.email, + full_name: payload.user.full_name, + id: payload.user.id, + }, + }; + } catch (_error: unknown) { + return null; + } +} diff --git a/apps/dashboard/next.config.ts b/apps/dashboard/next.config.ts index 212c17e..c04a64f 100644 --- a/apps/dashboard/next.config.ts +++ b/apps/dashboard/next.config.ts @@ -1,6 +1,22 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - transpilePackages: ["@basango/ui"], + devIndicators: false, + async headers() { + return [ + { + headers: [ + { + key: "X-Frame-Options", + value: "DENY", + }, + ], + source: "/((?!api/proxy).*)", + }, + ]; + }, + poweredByHeader: false, + reactStrictMode: true, + transpilePackages: ["@basango/ui", "@basango/api"], }; export default nextConfig; diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 59507c2..7485316 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,13 +1,28 @@ { "dependencies": { + "@basango/api": "workspace:*", "@basango/ui": "workspace:*", + "@date-fns/tz": "^1.4.1", + "@hookform/resolvers": "^5.2.2", + "@tanstack/react-query": "^5.90.7", + "@tanstack/react-table": "^8.21.3", + "@trpc/client": "^11.7.1", + "@trpc/tanstack-react-query": "^11.7.1", + "date-fns": "^4.1.0", "lucide-react": "^0.553.0", "next": "catalog:", + "next-international": "^1.3.1", "next-themes": "^0.4.6", + "nuqs": "^2.7.3", "react": "catalog:", - "react-dom": "catalog:" + "react-dom": "catalog:", + "react-hook-form": "^7.66.0", + "superjson": "^2.2.5", + "zod": "^4.1.12", + "zustand": "^5.0.8" }, "devDependencies": { + "@basango/tsconfig": "workspace:*", "@tailwindcss/postcss": "^4.1.17", "@types/bun": "catalog:", "@types/react": "catalog:", diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx new file mode 100644 index 0000000..a6b2cc9 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx @@ -0,0 +1,12 @@ +export default function Page() { + return ( +
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx new file mode 100644 index 0000000..0dfe7d1 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx @@ -0,0 +1,44 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@basango/ui/components/breadcrumb"; +import { Separator } from "@basango/ui/components/separator"; +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@basango/ui/components/sidebar"; + +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { HydrateClient } from "@/trpc/server"; + +export default async function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+
+ + + + + + Building Your Application + + + + Data Fetching + + + +
+
+ {children} +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx new file mode 100644 index 0000000..a6b2cc9 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx @@ -0,0 +1,12 @@ +export default function Page() { + return ( +
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/dashboard/src/app/[locale]/(public)/login/page.tsx b/apps/dashboard/src/app/[locale]/(public)/login/page.tsx new file mode 100644 index 0000000..cb9c391 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/(public)/login/page.tsx @@ -0,0 +1,32 @@ +import { GalleryVerticalEnd } from "lucide-react"; + +import { LoginForm } from "@/components/forms/login-form"; + +export default function LoginPage() { + return ( +
+
+ +
+
+ +
+
+
+
+ verification placeholder +
+
+ ); +} diff --git a/apps/dashboard/src/app/[locale]/error.tsx b/apps/dashboard/src/app/[locale]/error.tsx new file mode 100644 index 0000000..18e3dd1 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/error.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { Button } from "@basango/ui/components/button"; +import Link from "next/link"; + +export default function ErrorPage({ reset }: { reset: () => void }) { + return ( +
+
+
+

Something went wrong

+

+ An unexpected error has occurred. Please try again +
or contact support if the issue persists. +

+
+ +
+ + + + + +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/[locale]/layout.tsx b/apps/dashboard/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..2ad9226 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/layout.tsx @@ -0,0 +1,56 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "@basango/ui/globals.css"; + +import { Toaster } from "@basango/ui/components/sonner"; +import { NuqsAdapter } from "nuqs/adapters/next/app"; + +import { Providers } from "./providers"; + +const geistSans = Geist({ + subsets: ["latin"], + variable: "--font-geist-sans", +}); + +const geistMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-geist-mono", +}); + +export const metadata: Metadata = { + description: "Basango : The intelligent news curation platform.", + metadataBase: new URL("https://dashboard.basango.com"), + title: "Basango | AI-powered news curation dashboard", +}; + +export const viewport = { + initialScale: 1, + maximumScale: 1, + themeColor: [ + { media: "(prefers-color-scheme: light)" }, + { media: "(prefers-color-scheme: dark)" }, + ], + userScalable: false, + width: "device-width", +}; + +export default async function RootLayout({ + params, + children, +}: Readonly<{ + params: Promise<{ locale: string }>; + children: React.ReactNode; +}>) { + const { locale } = await params; + + return ( + + + + {children} + + + + + ); +} diff --git a/apps/dashboard/src/app/[locale]/not-found.tsx b/apps/dashboard/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..1c556fd --- /dev/null +++ b/apps/dashboard/src/app/[locale]/not-found.tsx @@ -0,0 +1,13 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+

Not Found

+

Could not find requested resource

+ + Return Home + +
+ ); +} diff --git a/apps/dashboard/src/app/[locale]/providers.tsx b/apps/dashboard/src/app/[locale]/providers.tsx new file mode 100644 index 0000000..0cc3398 --- /dev/null +++ b/apps/dashboard/src/app/[locale]/providers.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { ThemeProvider } from "next-themes"; +import type { ReactNode } from "react"; + +import { I18nProviderClient } from "@/locales/client"; +import { TRPCReactProvider } from "@/trpc/client"; + +type ProviderProps = { + locale: string; + children: ReactNode; +}; + +export function Providers({ locale, children }: ProviderProps) { + return ( + + + + {children} + + + + ); +} diff --git a/apps/dashboard/src/app/global-error.tsx b/apps/dashboard/src/app/global-error.tsx new file mode 100644 index 0000000..030d390 --- /dev/null +++ b/apps/dashboard/src/app/global-error.tsx @@ -0,0 +1,13 @@ +"use client"; + +import NextError from "next/error"; + +export default function GlobalError() { + return ( + + + + + + ); +} diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx deleted file mode 100644 index 64ebcf4..0000000 --- a/apps/dashboard/src/app/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "@basango/ui/global.css"; - -import { Providers } from "@/components/providers"; - -const geistSans = Geist({ - subsets: ["latin"], - variable: "--font-geist-sans", -}); - -const geistMono = Geist_Mono({ - subsets: ["latin"], - variable: "--font-geist-mono", -}); - -export const metadata: Metadata = { - description: "Basango : The intelligent news curation platform.", - title: "Basango Dashboard", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/apps/dashboard/src/app/page.tsx b/apps/dashboard/src/app/page.tsx deleted file mode 100644 index be29361..0000000 --- a/apps/dashboard/src/app/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Button } from "@basango/ui/components/button"; -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - -
-
-
- ); -} diff --git a/apps/dashboard/src/components/forms/login-form.tsx b/apps/dashboard/src/components/forms/login-form.tsx new file mode 100644 index 0000000..4bbf46e --- /dev/null +++ b/apps/dashboard/src/components/forms/login-form.tsx @@ -0,0 +1,59 @@ +import { Button } from "@basango/ui/components/button"; +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, + FieldSeparator, +} from "@basango/ui/components/field"; +import { Input } from "@basango/ui/components/input"; +import { cn } from "@basango/ui/lib/utils"; + +export function LoginForm({ className, ...props }: React.ComponentProps<"form">) { + return ( +
+ +
+

Login to your account

+

+ Enter your email below to login to your account +

+
+ + Email + + + + + + + + + + Or continue with + + + + Don't have an account?{" "} + + Sign up + + + +
+
+ ); +} diff --git a/apps/dashboard/src/components/providers.tsx b/apps/dashboard/src/components/providers.tsx deleted file mode 100644 index 62d6b84..0000000 --- a/apps/dashboard/src/components/providers.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import * as React from "react"; - -export function Providers({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} diff --git a/apps/dashboard/src/components/sidebar/app-sidebar.tsx b/apps/dashboard/src/components/sidebar/app-sidebar.tsx new file mode 100644 index 0000000..fd40b8e --- /dev/null +++ b/apps/dashboard/src/components/sidebar/app-sidebar.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, +} from "@basango/ui/components/sidebar"; +import { + AudioWaveform, + BookOpen, + Bot, + Command, + Frame, + GalleryVerticalEnd, + MapIcon, + PieChart, + Settings2, + SquareTerminal, +} from "lucide-react"; +import * as React from "react"; + +import { NavMain } from "./nav-main"; +import { NavProjects } from "./nav-projects"; +import { NavUser } from "./nav-user"; +import { TeamSwitcher } from "./team-switcher"; + +const data = { + navMain: [ + { + icon: SquareTerminal, + isActive: true, + items: [ + { + title: "History", + url: "#", + }, + { + title: "Starred", + url: "#", + }, + { + title: "Settings", + url: "#", + }, + ], + title: "Playground", + url: "#", + }, + { + icon: Bot, + items: [ + { + title: "Genesis", + url: "#", + }, + { + title: "Explorer", + url: "#", + }, + { + title: "Quantum", + url: "#", + }, + ], + title: "Models", + url: "#", + }, + { + icon: BookOpen, + items: [ + { + title: "Introduction", + url: "#", + }, + { + title: "Get Started", + url: "#", + }, + { + title: "Tutorials", + url: "#", + }, + { + title: "Changelog", + url: "#", + }, + ], + title: "Documentation", + url: "#", + }, + { + icon: Settings2, + items: [ + { + title: "General", + url: "#", + }, + { + title: "Team", + url: "#", + }, + { + title: "Billing", + url: "#", + }, + { + title: "Limits", + url: "#", + }, + ], + title: "Settings", + url: "#", + }, + ], + projects: [ + { + icon: Frame, + name: "Design Engineering", + url: "#", + }, + { + icon: PieChart, + name: "Sales & Marketing", + url: "#", + }, + { + icon: MapIcon, + name: "Travel", + url: "#", + }, + ], + teams: [ + { + logo: GalleryVerticalEnd, + name: "Acme Inc", + plan: "Enterprise", + }, + { + logo: AudioWaveform, + name: "Acme Corp.", + plan: "Startup", + }, + { + logo: Command, + name: "Evil Corp.", + plan: "Free", + }, + ], + user: { + avatar: "/avatars/shadcn.jpg", + email: "m@example.com", + name: "shadcn", + }, +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/apps/dashboard/src/components/sidebar/nav-main.tsx b/apps/dashboard/src/components/sidebar/nav-main.tsx new file mode 100644 index 0000000..ca320b9 --- /dev/null +++ b/apps/dashboard/src/components/sidebar/nav-main.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@basango/ui/components/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@basango/ui/components/sidebar"; +import { ChevronRight, type LucideIcon } from "lucide-react"; + +export function NavMain({ + items, +}: { + items: { + title: string; + url: string; + icon?: LucideIcon; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; + }[]; +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + ); +} diff --git a/apps/dashboard/src/components/sidebar/nav-projects.tsx b/apps/dashboard/src/components/sidebar/nav-projects.tsx new file mode 100644 index 0000000..85961a5 --- /dev/null +++ b/apps/dashboard/src/components/sidebar/nav-projects.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@basango/ui/components/dropdown-menu"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@basango/ui/components/sidebar"; +import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react"; + +export function NavProjects({ + projects, +}: { + projects: { + name: string; + url: string; + icon: LucideIcon; + }[]; +}) { + const { isMobile } = useSidebar(); + + return ( + + Projects + + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ); +} diff --git a/apps/dashboard/src/components/sidebar/nav-user.tsx b/apps/dashboard/src/components/sidebar/nav-user.tsx new file mode 100644 index 0000000..bf11278 --- /dev/null +++ b/apps/dashboard/src/components/sidebar/nav-user.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { Avatar, AvatarFallback, AvatarImage } from "@basango/ui/components/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@basango/ui/components/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@basango/ui/components/sidebar"; +import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from "lucide-react"; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ); +} diff --git a/apps/dashboard/src/components/sidebar/team-switcher.tsx b/apps/dashboard/src/components/sidebar/team-switcher.tsx new file mode 100644 index 0000000..4752a16 --- /dev/null +++ b/apps/dashboard/src/components/sidebar/team-switcher.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@basango/ui/components/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@basango/ui/components/sidebar"; +import { ChevronsUpDown, Plus } from "lucide-react"; +import * as React from "react"; + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string; + logo: React.ElementType; + plan: string; + }[]; +}) { + const { isMobile } = useSidebar(); + const [activeTeam, setActiveTeam] = React.useState(teams[0]); + + if (!activeTeam) { + return null; + } + + return ( + + + + + +
+ +
+
+ {activeTeam.name} + {activeTeam.plan} +
+ +
+
+ + Teams + {teams.map((team, index) => ( + setActiveTeam(team)} + > +
+ +
+ {team.name} + ⌘{index + 1} +
+ ))} + + +
+ +
+
Add team
+
+
+
+
+
+ ); +} diff --git a/apps/dashboard/src/hooks/use-calendar-dates.ts b/apps/dashboard/src/hooks/use-calendar-dates.ts new file mode 100644 index 0000000..ab67e16 --- /dev/null +++ b/apps/dashboard/src/hooks/use-calendar-dates.ts @@ -0,0 +1,30 @@ +import { TZDate } from "@date-fns/tz"; +import { eachDayOfInterval, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from "date-fns"; + +export function useCalendarDates(currentDate: Date, weekStartsOnMonday: boolean) { + const monthStart = startOfMonth(currentDate); + const monthEnd = endOfMonth(currentDate); + const calendarStart = startOfWeek(monthStart, { + weekStartsOn: weekStartsOnMonday ? 1 : 0, + }); + const calendarEnd = endOfWeek(monthEnd, { + weekStartsOn: weekStartsOnMonday ? 1 : 0, + }); + const calendarDays = eachDayOfInterval({ + end: calendarEnd, + start: calendarStart, + }).map((date) => new TZDate(date, "UTC")); + const firstWeek = eachDayOfInterval({ + end: endOfWeek(calendarStart, { weekStartsOn: weekStartsOnMonday ? 1 : 0 }), + start: calendarStart, + }).map((date) => new TZDate(date, "UTC")); + + return { + calendarDays, + calendarEnd, + calendarStart, + firstWeek, + monthEnd, + monthStart, + }; +} diff --git a/apps/dashboard/src/hooks/use-local-storage.ts b/apps/dashboard/src/hooks/use-local-storage.ts new file mode 100644 index 0000000..3a845fc --- /dev/null +++ b/apps/dashboard/src/hooks/use-local-storage.ts @@ -0,0 +1,69 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +// Original useLocalStorage hook (useState-like API) +export function useLocalStorage( + key: string, + initialValue: T, +): [T, (value: T | ((val: T) => T)) => void] { + // Initialize state with a function to handle SSR + const [localState, setLocalState] = useState(() => { + // Check if we're in a browser environment + if (typeof window === "undefined") { + return initialValue; + } + + try { + // Get from local storage by key + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + // Return a wrapped version of useState's setter function that persists the new value to localStorage + const handleSetState = useCallback( + (value: T | ((val: T) => T)) => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = value instanceof Function ? value(localState) : value; + // Save state + setLocalState(valueToStore); + // Save to local storage + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }, + [key, localState], + ); + + useEffect(() => { + // Handle storage changes in other tabs/windows + function handleStorageChange(event: StorageEvent) { + if (event.key === key && event.newValue) { + setLocalState(JSON.parse(event.newValue)); + } + } + + // Subscribe to storage changes + if (typeof window !== "undefined") { + window.addEventListener("storage", handleStorageChange); + } + + // Cleanup the event listener on component unmount + return () => { + if (typeof window !== "undefined") { + window.removeEventListener("storage", handleStorageChange); + } + }; + }, [key]); + + return [localState, handleSetState]; +} diff --git a/apps/dashboard/src/hooks/use-zod-form.ts b/apps/dashboard/src/hooks/use-zod-form.ts new file mode 100644 index 0000000..3876065 --- /dev/null +++ b/apps/dashboard/src/hooks/use-zod-form.ts @@ -0,0 +1,14 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: This is a hook file */ +import { zodResolver } from "@hookform/resolvers/zod"; +import { type Resolver, type UseFormProps, useForm } from "react-hook-form"; +import type { z } from "zod"; + +export const useZodForm = >( + schema: T, + options?: Omit>, "resolver">, +) => { + return useForm>({ + resolver: zodResolver(schema) as unknown as Resolver, any, z.infer>, + ...options, + }); +}; diff --git a/apps/dashboard/src/locales/client.ts b/apps/dashboard/src/locales/client.ts new file mode 100644 index 0000000..1cd00a5 --- /dev/null +++ b/apps/dashboard/src/locales/client.ts @@ -0,0 +1,11 @@ +"use client"; + +import { createI18nClient } from "next-international/client"; + +// NOTE: Also update middleware.ts to support locale +export const languages = ["en"]; + +export const { useScopedI18n, I18nProviderClient, useCurrentLocale, useChangeLocale, useI18n } = + createI18nClient({ + en: () => import("./translations/en"), + }); diff --git a/apps/dashboard/src/locales/server.ts b/apps/dashboard/src/locales/server.ts new file mode 100644 index 0000000..c4a5173 --- /dev/null +++ b/apps/dashboard/src/locales/server.ts @@ -0,0 +1,5 @@ +import { createI18nServer } from "next-international/server"; + +export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({ + en: () => import("./translations/en"), +}); diff --git a/apps/dashboard/src/locales/translations/en.ts b/apps/dashboard/src/locales/translations/en.ts new file mode 100644 index 0000000..bacc861 --- /dev/null +++ b/apps/dashboard/src/locales/translations/en.ts @@ -0,0 +1,3 @@ +export default { + app: "Basango Dashboard", +} as const; diff --git a/apps/dashboard/src/proxy.ts b/apps/dashboard/src/proxy.ts new file mode 100644 index 0000000..6a89d25 --- /dev/null +++ b/apps/dashboard/src/proxy.ts @@ -0,0 +1,50 @@ +import { type NextRequest } from "next/server"; +import { createI18nMiddleware } from "next-international/middleware"; + +const I18nMiddleware = createI18nMiddleware({ + defaultLocale: "en", + locales: ["en"], + urlMappingStrategy: "rewrite", +}); + +export default async function proxy(request: NextRequest) { + const response = await I18nMiddleware(request); + + // const nextUrl = request.nextUrl; + + // const pathnameLocale = nextUrl.pathname.split("/", 2)?.[1]; + + // // Remove the locale from the pathname + // const pathnameWithoutLocale = pathnameLocale + // ? nextUrl.pathname.slice(pathnameLocale.length + 1) + // : nextUrl.pathname; + + // // Create a new URL without the locale in the pathname + // const newUrl = new URL(pathnameWithoutLocale || "/", request.url); + // const encodedSearchParams = `${newUrl?.pathname?.substring(1)}${newUrl.search}`; + // const session = request.cookies.get("token")?.value; + + // // 1. Not authenticated + // if ( + // !session && + // newUrl.pathname !== "/login" && + // !newUrl.pathname.includes("/i/") && + // !newUrl.pathname.includes("/s/") && + // !newUrl.pathname.includes("/verify") + // ) { + // const url = new URL("/login", request.url); + + // if (encodedSearchParams) { + // url.searchParams.append("return_to", encodedSearchParams); + // } + + // return NextResponse.redirect(url); + // } + + // If all checks pass, return the original or updated response + return response; +} + +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"], +}; diff --git a/apps/dashboard/src/trpc/client.tsx b/apps/dashboard/src/trpc/client.tsx new file mode 100644 index 0000000..ee1fd5f --- /dev/null +++ b/apps/dashboard/src/trpc/client.tsx @@ -0,0 +1,67 @@ +"use client"; + +import type { AppRouter } from "@basango/api/trpc/routers/_app"; +import type { QueryClient } from "@tanstack/react-query"; +import { QueryClientProvider, isServer } from "@tanstack/react-query"; +import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import { useState } from "react"; +import superjson from "superjson"; + +import { makeQueryClient } from "./query-client"; + +export const { TRPCProvider, useTRPC } = createTRPCContext(); + +let browserQueryClient: QueryClient; + +function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } + + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + + return browserQueryClient; +} + +export function TRPCReactProvider( + props: Readonly<{ + children: React.ReactNode; + }>, +) { + const queryClient = getQueryClient(); + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + httpBatchLink({ + async headers() { + const token = window.localStorage.getItem("auth_token"); + return { + Authorization: `Bearer ${token}`, + }; + }, + transformer: superjson, + url: `${process.env.NEXT_PUBLIC_API_URL}/trpc`, + }), + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); +} diff --git a/apps/dashboard/src/trpc/query-client.ts b/apps/dashboard/src/trpc/query-client.ts new file mode 100644 index 0000000..1698163 --- /dev/null +++ b/apps/dashboard/src/trpc/query-client.ts @@ -0,0 +1,20 @@ +import { QueryClient, defaultShouldDehydrateQuery } from "@tanstack/react-query"; +import superjson from "superjson"; + +export function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + dehydrate: { + serializeData: superjson.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || query.state.status === "pending", + }, + hydrate: { + deserializeData: superjson.deserialize, + }, + queries: { + staleTime: 60 * 1000, + }, + }, + }); +} diff --git a/apps/dashboard/src/trpc/server.tsx b/apps/dashboard/src/trpc/server.tsx new file mode 100644 index 0000000..48ad845 --- /dev/null +++ b/apps/dashboard/src/trpc/server.tsx @@ -0,0 +1,80 @@ +import "server-only"; + +import type { AppRouter } from "@basango/api/trpc/routers/_app"; +//import { getCountryCode, getLocale, getTimezone } from "@basango/location"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client"; +import { type TRPCQueryOptions, createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import { cache } from "react"; +import superjson from "superjson"; + +import { makeQueryClient } from "./query-client"; + +// IMPORTANT: Create a stable getter for the query client that +// will return the same client during the same request. +export const getQueryClient = cache(makeQueryClient); + +export const trpc = createTRPCOptionsProxy({ + client: createTRPCClient({ + links: [ + httpBatchLink({ + async headers() { + const token = window.localStorage.getItem("auth_token"); + + return { + Authorization: `Bearer ${token}`, + // "x-user-country": await getCountryCode(), + // "x-user-locale": await getLocale(), + // "x-user-timezone": await getTimezone(), + }; + }, + transformer: superjson, + url: `${process.env.NEXT_PUBLIC_API_URL}/trpc`, + }), + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + }), + ], + }), + queryClient: getQueryClient, +}); + +export function HydrateClient(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + return {props.children}; +} + +export function prefetch>>(queryOptions: T) { + const queryClient = getQueryClient(); + + if (queryOptions.queryKey[1]?.type === "infinite") { + void queryClient.prefetchInfiniteQuery( + queryOptions as unknown as Parameters[0], + ); + } else { + void queryClient.prefetchQuery( + queryOptions as unknown as Parameters[0], + ); + } +} + +export function batchPrefetch>>( + queryOptionsArray: T[], +) { + const queryClient = getQueryClient(); + + for (const queryOptions of queryOptionsArray) { + if (queryOptions.queryKey[1]?.type === "infinite") { + void queryClient.prefetchInfiniteQuery( + queryOptions as unknown as Parameters[0], + ); + } else { + void queryClient.prefetchQuery( + queryOptions as unknown as Parameters[0], + ); + } + } +} diff --git a/apps/dashboard/src/utils/categories.ts b/apps/dashboard/src/utils/categories.ts new file mode 100644 index 0000000..1a922fd --- /dev/null +++ b/apps/dashboard/src/utils/categories.ts @@ -0,0 +1,101 @@ +export const colors = [ + "#FF6900", // Orange + "#FCB900", // Yellow + "#00D084", // Emerald + "#8ED1FC", // Sky Blue + "#0693E3", // Blue + "#ABB8C3", // Gray + "#EB144C", // Red + "#F78DA7", // Pink + "#9900EF", // Purple + "#0079BF", // Dark Blue + "#B6BBBF", // Light Gray + "#FF5A5F", // Coral + "#F7C59F", // Peach + "#8492A6", // Slate + "#4D5055", // Charcoal + "#AF5A50", // Terracotta + "#F9D6E7", // Pale Pink + "#B5EAEA", // Pale Cyan + "#B388EB", // Lavender + "#B04632", // Rust + "#FF78CB", // Pink + "#4E5A65", // Gray + "#01FF70", // Lime + "#85144b", // Pink + "#F012BE", // Purple + "#7FDBFF", // Sky Blue + "#3D9970", // Olive + "#AAAAAA", // Silver + "#111111", // Black + "#0074D9", // Blue + "#39CCCC", // Teal + "#001f3f", // Navy + "#FF9F1C", // Orange + "#5E6A71", // Ash + "#75D701", // Neon Green + "#B6C8A9", // Lichen + "#00A9FE", // Electric Blue + "#EAE8E1", // Bone + "#CD346C", // Raspberry + "#FF6FA4", // Pink Sherbet + "#D667FB", // Purple Mountain Majesty + "#0080FF", // Azure + "#656D78", // Dim Gray + "#F8842C", // Tangerine + "#FF8CFF", // Carnation Pink + "#647F6A", // Feldgrau + "#5E574E", // Field Drab + "#EF5466", // KU Crimson + "#B0E0E6", // Powder Blue + "#EB5E7C", // Rose Pink + "#8A2BE2", // Blue Violet + "#6B7C85", // Slate Gray + "#8C92AC", // Lavender Blue + "#6C587A", // Eminence + "#52A1FF", // Azureish White + "#32CD32", // Lime Green + "#E04F9F", // Orchid Pink + "#915C83", // Lilac Bush + "#4C6B88", // Air Force Blue + "#587376", // Cadet Blue + "#C46210", // Buff + "#65B0D0", // Columbia Blue + "#2F4F4F", // Dark Slate Gray + "#528B8B", // Dark Cyan + "#8B4513", // Saddle Brown + "#4682B4", // Steel Blue + "#CD853F", // Peru + "#FFA07A", // Light Salmon + "#CD5C5C", // Indian Red + "#483D8B", // Dark Slate Blue + "#696969", // Dim Gray +]; + +export function customHash(value: string) { + let hash = 0; + + for (let i = 0; i < value.length; i++) { + hash = (hash << 5) + value.charCodeAt(i); + hash = hash & hash; + } + + return Math.abs(hash); +} + +export function getColor(value: string, arrayLength: number) { + const hashValue = customHash(value); + const index = hashValue % arrayLength; + return index; +} + +export function getColorFromName(value: string) { + const index = getColor(value, colors.length); + + return colors[index]; +} + +export function getRandomColor() { + const randomIndex = Math.floor(Math.random() * colors.length); + return colors[randomIndex]; +} diff --git a/apps/dashboard/src/utils/environment.ts b/apps/dashboard/src/utils/environment.ts new file mode 100644 index 0000000..35b1105 --- /dev/null +++ b/apps/dashboard/src/utils/environment.ts @@ -0,0 +1,11 @@ +export function getUrl() { + if (process.env.NEXT_PUBLIC_URL) { + return process.env.NEXT_PUBLIC_URL; + } + + if (process.env.VERCEL_TARGET_ENV === "preview") { + return `https://${process.env.VERCEL_URL}`; + } + + return "http://localhost:3000"; +} diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index 4748a0f..f4b282d 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -13,5 +13,12 @@ }, "exclude": ["node_modules"], "extends": "@basango/tsconfig/nextjs.json", - "include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"] + "include": [ + "next-env.d.ts", + "next.config.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "../../packages/ui/src/hooks/use-mobile.ts" + ] } diff --git a/biome.json b/biome.json index 60b4801..516d0ac 100644 --- a/biome.json +++ b/biome.json @@ -57,7 +57,10 @@ "enabled": true, "rules": { "a11y": { - "useSemanticElements": "off" + "noSvgWithoutTitle": "off", + "useFocusableInteractive": "warn", + "useSemanticElements": "off", + "useValidAnchor": "off" }, "correctness": { "noUnusedImports": "on", @@ -69,7 +72,8 @@ "useImportType": "off" }, "suspicious": { - "noArrayIndexKey": "off" + "noArrayIndexKey": "off", + "noDocumentCookie": "off" } } }, diff --git a/bun.lock b/bun.lock index c61fa2b..22bc87c 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "commitizen": "^4.3.1", "cz-conventional-changelog": "^3.3.0", "husky": "^9.1.7", + "superjson": "^2.2.5", "turbo": "^2.5.8", "typescript": "catalog:", }, @@ -26,6 +27,7 @@ "@basango/encryption": "workspace:*", "@basango/logger": "workspace:*", "@hono/node-server": "^1.19.6", + "@hono/trpc-server": "^0.4.0", "@hono/zod-openapi": "^1.1.4", "@scalar/hono-api-reference": "^0.9.24", "@trpc/server": "^11.7.1", @@ -60,14 +62,29 @@ "apps/dashboard": { "name": "@basango/dashboard", "dependencies": { + "@basango/api": "workspace:*", "@basango/ui": "workspace:*", + "@date-fns/tz": "^1.4.1", + "@hookform/resolvers": "^5.2.2", + "@tanstack/react-query": "^5.90.7", + "@tanstack/react-table": "^8.21.3", + "@trpc/client": "^11.7.1", + "@trpc/tanstack-react-query": "^11.7.1", + "date-fns": "^4.1.0", "lucide-react": "^0.553.0", "next": "catalog:", + "next-international": "^1.3.1", "next-themes": "^0.4.6", + "nuqs": "^2.7.3", "react": "catalog:", "react-dom": "catalog:", + "react-hook-form": "^7.66.0", + "superjson": "^2.2.5", + "zod": "^4.1.12", + "zustand": "^5.0.8", }, "devDependencies": { + "@basango/tsconfig": "workspace:*", "@tailwindcss/postcss": "^4.1.17", "@types/bun": "catalog:", "@types/react": "catalog:", @@ -146,8 +163,11 @@ "name": "@basango/ui", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -158,6 +178,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -165,6 +186,7 @@ "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.6", "zod": "^4.1.12", @@ -468,6 +490,8 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], + "@date-fns/utc": ["@date-fns/utc@2.1.1", "", {}, "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="], "@devscast/config": ["@devscast/config@1.0.3", "", { "peerDependencies": { "ini": "^6.0.0", "yaml": "^2.8.1", "zod": "^4.1.12" }, "optionalPeers": ["ini", "yaml"] }, "sha512-/FjCA/MV1KR2tY44YBA4tdXNzQgoF75O+RQ4fbzvVWY77PXOama2Hf6YXeLcQsvxfItaXi2cFz8BaaVdqZYS8w=="], @@ -596,10 +620,14 @@ "@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="], + "@hono/trpc-server": ["@hono/trpc-server@0.4.0", "", { "peerDependencies": { "@trpc/server": "^10.10.0 || >11.0.0-rc", "hono": ">=4.*" } }, "sha512-LGlJfCmNIGMwcknZEIYdujVMs9OkNVazhpOhaz3kTWOXvNL660VOHpvvktosCiJrajyBY1RtIJKQ+IKaQvNuSg=="], + "@hono/zod-openapi": ["@hono/zod-openapi@1.1.4", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^8.1.0", "@hono/zod-validator": "^0.7.4", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "^4.0.0" } }, "sha512-4BbOtd6oKg20yo6HLluVbEycBLLIfdKX5o/gUSoKZ2uBmeP4Og/VDfIX3k9pbNEX5W3fRkuPeVjGA+zaQDVY1A=="], "@hono/zod-validator": ["@hono/zod-validator@0.7.4", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -752,8 +780,12 @@ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -766,6 +798,8 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], @@ -776,6 +810,8 @@ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], @@ -802,6 +838,8 @@ "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -810,6 +848,8 @@ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], @@ -916,6 +956,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="], @@ -948,10 +990,22 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="], + + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + "@trpc/client": ["@trpc/client@11.7.1", "", { "peerDependencies": { "@trpc/server": "11.7.1", "typescript": ">=5.7.2" } }, "sha512-uOnAjElKI892/U6aQMcBHYs3x7mme3Cvv1F87ytBL56rBvs7+DyK7r43zgaXKf13+GtPEI6ex5xjVUfyDW8XcQ=="], + "@trpc/server": ["@trpc/server@11.7.1", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-N3U8LNLIP4g9C7LJ/sLkjuPHwqlvE3bnspzC4DEFVdvx2+usbn70P80E3wj5cjOTLhmhRiwJCSXhlB+MHfGeCw=="], + "@trpc/tanstack-react-query": ["@trpc/tanstack-react-query@11.7.1", "", { "peerDependencies": { "@tanstack/react-query": "^5.80.3", "@trpc/client": "11.7.1", "@trpc/server": "11.7.1", "react": ">=18.2.0", "react-dom": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-qc7kz4NY7CCvCxLy5HGptfKd3e3yJnWmTd6/Gkr4IY8B73PNFmcHKvLWE4kzU7r+R72MfT57TXrCEJ7ErLSMtw=="], + "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], @@ -1248,6 +1302,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + "core-js-compat": ["core-js-compat@3.46.0", "", { "dependencies": { "browserslist": "^4.26.3" } }, "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law=="], "core-js-pure": ["core-js-pure@3.46.0", "", {}, "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw=="], @@ -1598,6 +1654,8 @@ "inquirer": ["inquirer@8.2.5", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^7.0.0" } }, "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ=="], + "international-types": ["international-types@0.8.1", "", {}, "sha512-tajBCAHo4I0LIFlmQ9ZWfjMWVyRffzuvfbXCd6ssFt5u1Zw15DN0UBpVTItXdNa1ls+cpQt3Yw8+TxsfGF8JcA=="], + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], @@ -1640,6 +1698,8 @@ "is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="], + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -1886,6 +1946,8 @@ "next": ["next@16.0.1", "", { "dependencies": { "@next/env": "16.0.1", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.1", "@next/swc-darwin-x64": "16.0.1", "@next/swc-linux-arm64-gnu": "16.0.1", "@next/swc-linux-arm64-musl": "16.0.1", "@next/swc-linux-x64-gnu": "16.0.1", "@next/swc-linux-x64-musl": "16.0.1", "@next/swc-win32-arm64-msvc": "16.0.1", "@next/swc-win32-x64-msvc": "16.0.1", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw=="], + "next-international": ["next-international@1.3.1", "", { "dependencies": { "client-only": "^0.0.1", "international-types": "^0.8.1", "server-only": "^0.0.1" } }, "sha512-ydU9jQe+4MohMWltbZae/yuWeKhmp0QKQqJNNi8WCCMwrly03qfMAHw/tWbT2qgAlG++CxF5jMXmGQZgOHeVOw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], @@ -1916,6 +1978,8 @@ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + "nuqs": ["nuqs@2.7.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^6 || ^7", "react-router-dom": "^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-lQzSYLsXUYftc0cerww64yevjMeYOX8thkcqI25XtyyTEJFTk3LE+i2hcY2h0Jwvp1+owCb+KJ0GMaKQwfmq3g=="], + "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2082,6 +2146,8 @@ "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], + "react-hook-form": ["react-hook-form@7.66.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw=="], + "react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="], "react-native": ["react-native@0.81.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", "@react-native/codegen": "0.81.5", "@react-native/community-cli-plugin": "0.81.5", "@react-native/gradle-plugin": "0.81.5", "@react-native/js-polyfills": "0.81.5", "@react-native/normalize-colors": "0.81.5", "@react-native/virtualized-lists": "0.81.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw=="], @@ -2236,6 +2302,8 @@ "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -2292,6 +2360,8 @@ "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + "superjson": ["superjson@2.2.5", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w=="], + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], @@ -2510,6 +2580,8 @@ "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], + "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], + "@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -2530,6 +2602,8 @@ "@babel/traverse--for-generate-function-map/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@basango/dashboard/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "@commitlint/load/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -2616,18 +2690,26 @@ "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], + + "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@scalar/openapi-types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], "@scalar/types/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], diff --git a/package.json b/package.json index ce68e1b..fd09089 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "commitizen": "^4.3.1", "cz-conventional-changelog": "^3.3.0", "husky": "^9.1.7", + "superjson": "^2.2.5", "turbo": "^2.5.8", "typescript": "catalog:" }, diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts index b55dfaa..27ce76f 100644 --- a/packages/db/drizzle.config.ts +++ b/packages/db/drizzle.config.ts @@ -1,7 +1,6 @@ -import { createEnvAccessor } from "@devscast/config"; import { defineConfig } from "drizzle-kit"; -const env = createEnvAccessor(["BASANGO_DATABASE_URL"] as const); +import { env } from "./src/config"; export default defineConfig({ dbCredentials: { diff --git a/packages/db/src/config.ts b/packages/db/src/config.ts index e4bb166..df46ad7 100644 --- a/packages/db/src/config.ts +++ b/packages/db/src/config.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { loadConfig } from "@devscast/config"; import { z } from "zod"; -const PROJECT_DIR = path.resolve(__dirname, "../../"); +const PROJECT_DIR = path.resolve(__dirname, "../"); export const { env, config } = loadConfig({ env: { diff --git a/packages/db/src/queries/sources.ts b/packages/db/src/queries/sources.ts index d662be4..ed1e2cd 100644 --- a/packages/db/src/queries/sources.ts +++ b/packages/db/src/queries/sources.ts @@ -5,13 +5,16 @@ import { Database } from "@/client"; import { NotFoundError } from "@/errors"; import { Credibility, source } from "@/schema"; +export async function getSources(db: Database) { + return db.query.source.findMany(); +} + export type CreateSourceParams = { name: string; url: string; displayName?: string; description?: string; - credibility: Credibility; - updatedAt?: Date; + credibility?: Credibility; }; export async function createSource(db: Database, params: CreateSourceParams) { @@ -23,6 +26,33 @@ export async function createSource(db: Database, params: CreateSourceParams) { return result; } +export type UpdateSourceParams = { + id: string; + name?: string; + displayName?: string; + description?: string; + credibility?: Credibility; +}; + +export async function updateSource(db: Database, params: UpdateSourceParams) { + const [result] = await db + .update(source) + .set({ + credibility: params.credibility, + description: params.description, + displayName: params.displayName, + name: params.name, + }) + .where(eq(source.id, params.id)) + .returning(); + + if (result === undefined) { + throw new NotFoundError(`Source not found`); + } + + return result; +} + export type DeleteSourceParams = { id: string; }; @@ -39,6 +69,12 @@ export async function getSourceByName(db: Database, name: string) { }); } +export async function getById(db: Database, id: string) { + return db.query.source.findFirst({ + where: eq(source.id, id), + }); +} + export async function getSourceIdByName(db: Database, name: string): Promise { const result = await db.query.source.findFirst({ columns: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 5c288ea..4a66fee 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,8 +1,11 @@ { "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -13,6 +16,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -20,6 +24,7 @@ "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.6", "zod": "^4.1.12" @@ -36,7 +41,7 @@ }, "exports": { "./components/*": "./src/components/*.tsx", - "./global.css": "./src/styles/global.css", + "./globals.css": "./src/styles/globals.css", "./hooks/*": "./src/hooks/*.ts", "./lib/*": "./src/lib/*.ts", "./postcss.config": "./postcss.config.mjs" diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx new file mode 100644 index 0000000..16060bd --- /dev/null +++ b/packages/ui/src/components/avatar.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { cn } from "@basango/ui/lib/utils"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as React from "react"; + +function Avatar({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/packages/ui/src/components/breadcrumb.tsx b/packages/ui/src/components/breadcrumb.tsx new file mode 100644 index 0000000..c0dc900 --- /dev/null +++ b/packages/ui/src/components/breadcrumb.tsx @@ -0,0 +1,101 @@ +import { cn } from "@basango/ui/lib/utils"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return