From f870c2e62eb404ac574b2e60f620f6de7268751c Mon Sep 17 00:00:00 2001 From: Berkay Date: Sun, 16 Nov 2025 20:35:19 +0300 Subject: [PATCH] users update create delete tested --- backend/src/users/dto/create-user.input.ts | 12 + backend/src/users/dto/update-user.input.ts | 53 ++ backend/src/users/users.resolver.ts | 9 +- backend/src/users/users.service.ts | 11 +- frontend/app/api/users/add/route.ts | 53 ++ frontend/app/api/users/add/schema.ts | 29 + frontend/app/api/users/delete/route.ts | 22 + frontend/app/api/users/list/route.ts | 3 +- frontend/app/api/users/update/route.ts | 35 ++ frontend/app/api/users/update/schema.ts | 28 + frontend/app/dashboard/page.tsx | 5 +- frontend/app/layout.tsx | 31 +- frontend/app/users/add/page.tsx | 5 + frontend/app/users/update/page.tsx | 7 + frontend/components/dashboard/data-table.tsx | 1 - .../components/dashboard/nav-documents.tsx | 10 +- frontend/components/dashboard/nav-main.tsx | 49 +- frontend/components/dashboard/site-header.tsx | 3 +- frontend/components/sidebar/app-sidebar.tsx | 183 +++++++ .../components/sidebar/navs/nav-documents.tsx | 92 ++++ frontend/components/sidebar/navs/nav-main.tsx | 58 ++ .../components/sidebar/navs/nav-secondary.tsx | 42 ++ frontend/components/sidebar/navs/nav-user.tsx | 110 ++++ frontend/components/sidebar/site-header.tsx | 29 + frontend/components/ui/calendar.tsx | 216 ++++++++ frontend/components/ui/chart.tsx | 20 +- frontend/components/ui/date-time-picker.tsx | 219 ++++++++ frontend/components/ui/dropdown-menu.tsx | 14 +- frontend/components/ui/form.tsx | 167 ++++++ frontend/components/ui/popover.tsx | 48 ++ frontend/components/ui/select.tsx | 10 +- frontend/components/ui/table.tsx | 4 +- frontend/components/ui/tooltip.tsx | 2 +- frontend/lib/utils.ts | 6 + frontend/package-lock.json | 494 ++++++++++++++---- frontend/package.json | 5 + frontend/pages/dashboard/page.tsx | 4 +- frontend/pages/users/add/form.tsx | 289 ++++++++++ frontend/pages/users/add/page.tsx | 26 + frontend/pages/users/add/queries.tsx | 21 + frontend/pages/users/add/schema.ts | 29 + frontend/pages/users/add/table/columns.tsx | 267 ++++++++++ frontend/pages/users/add/table/data-table.tsx | 287 ++++++++++ frontend/pages/users/add/table/schema.tsx | 47 ++ frontend/pages/users/add/types.ts | 24 + frontend/pages/users/page.tsx | 38 +- frontend/pages/users/queries.tsx | 22 +- frontend/pages/users/table/columns.tsx | 267 ++++++++++ frontend/pages/users/table/data-table.tsx | 280 ++++++++++ frontend/pages/users/table/schema.tsx | 47 ++ frontend/pages/users/update/form.tsx | 250 +++++++++ frontend/pages/users/update/page.tsx | 46 ++ frontend/pages/users/update/queries.tsx | 21 + frontend/pages/users/update/schema.ts | 27 + frontend/pages/users/update/table/columns.tsx | 267 ++++++++++ .../pages/users/update/table/data-table.tsx | 287 ++++++++++ frontend/pages/users/update/table/schema.tsx | 47 ++ frontend/pages/users/update/types.ts | 24 + 58 files changed, 4511 insertions(+), 191 deletions(-) create mode 100644 backend/src/users/dto/update-user.input.ts create mode 100644 frontend/app/api/users/add/route.ts create mode 100644 frontend/app/api/users/add/schema.ts create mode 100644 frontend/app/api/users/delete/route.ts create mode 100644 frontend/app/api/users/update/route.ts create mode 100644 frontend/app/api/users/update/schema.ts create mode 100644 frontend/app/users/add/page.tsx create mode 100644 frontend/app/users/update/page.tsx create mode 100644 frontend/components/sidebar/app-sidebar.tsx create mode 100644 frontend/components/sidebar/navs/nav-documents.tsx create mode 100644 frontend/components/sidebar/navs/nav-main.tsx create mode 100644 frontend/components/sidebar/navs/nav-secondary.tsx create mode 100644 frontend/components/sidebar/navs/nav-user.tsx create mode 100644 frontend/components/sidebar/site-header.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/date-time-picker.tsx create mode 100644 frontend/components/ui/form.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/pages/users/add/form.tsx create mode 100644 frontend/pages/users/add/page.tsx create mode 100644 frontend/pages/users/add/queries.tsx create mode 100644 frontend/pages/users/add/schema.ts create mode 100644 frontend/pages/users/add/table/columns.tsx create mode 100644 frontend/pages/users/add/table/data-table.tsx create mode 100644 frontend/pages/users/add/table/schema.tsx create mode 100644 frontend/pages/users/add/types.ts create mode 100644 frontend/pages/users/table/columns.tsx create mode 100644 frontend/pages/users/table/data-table.tsx create mode 100644 frontend/pages/users/table/schema.tsx create mode 100644 frontend/pages/users/update/form.tsx create mode 100644 frontend/pages/users/update/page.tsx create mode 100644 frontend/pages/users/update/queries.tsx create mode 100644 frontend/pages/users/update/schema.ts create mode 100644 frontend/pages/users/update/table/columns.tsx create mode 100644 frontend/pages/users/update/table/data-table.tsx create mode 100644 frontend/pages/users/update/table/schema.tsx create mode 100644 frontend/pages/users/update/types.ts diff --git a/backend/src/users/dto/create-user.input.ts b/backend/src/users/dto/create-user.input.ts index 506ba1b..12ab114 100644 --- a/backend/src/users/dto/create-user.input.ts +++ b/backend/src/users/dto/create-user.input.ts @@ -44,4 +44,16 @@ export class CreateUserInput { @Field(() => ID, { nullable: true }) type?: string; + + @Field(() => ID, { nullable: true }) + expiryStarts?: string; + + @Field(() => ID, { nullable: true }) + expiryEnds?: string; + + @Field(() => Boolean, { nullable: true }) + isConfirmed?: boolean; + + @Field(() => Boolean, { nullable: true }) + isNotificationSend?: boolean; } diff --git a/backend/src/users/dto/update-user.input.ts b/backend/src/users/dto/update-user.input.ts new file mode 100644 index 0000000..b26b0b0 --- /dev/null +++ b/backend/src/users/dto/update-user.input.ts @@ -0,0 +1,53 @@ +import { InputType, Field, ID } from '@nestjs/graphql'; + +@InputType() +export class UpdateCollectionTokenItemInput { + @Field({ nullable: true }) + prefix?: string; + + @Field({ nullable: true }) + token?: string; +} + +@InputType() +export class UpdateCollectionTokenInput { + @Field(() => [UpdateCollectionTokenItemInput], { nullable: true }) + tokens?: UpdateCollectionTokenItemInput[]; + + @Field({ nullable: true }) + default?: string; +} + +@InputType() +export class UpdateUserInput { + + @Field({ nullable: true }) + tag?: string; + + @Field({ nullable: true }) + email?: string; + + @Field({ nullable: true }) + phone?: string; + + @Field(() => UpdateCollectionTokenInput, { nullable: true }) + collectionTokens?: UpdateCollectionTokenInput; + + @Field(() => ID, { nullable: true }) + person?: string; + + @Field(() => ID, { nullable: true }) + type?: string; + + @Field({ nullable: true }) + expiryStarts?: string; + + @Field({ nullable: true }) + expiryEnds?: string; + + @Field(() => Boolean, { nullable: true }) + isConfirmed?: boolean; + + @Field(() => Boolean, { nullable: true }) + isNotificationSend?: boolean; +} diff --git a/backend/src/users/users.resolver.ts b/backend/src/users/users.resolver.ts index a169a0e..6ffbcab 100644 --- a/backend/src/users/users.resolver.ts +++ b/backend/src/users/users.resolver.ts @@ -1,10 +1,11 @@ -import { Resolver, Query, Args, ID, Info, Mutation, Int } from '@nestjs/graphql'; +import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; import { Types } from 'mongoose'; import { User } from '@/models/user.model'; import { UsersService } from '@/users/users.service'; import { CreateUserInput } from './dto/create-user.input'; import { ListArguments } from '@/dto/list.input'; import { UsersListResponse } from '@/people/dto/list-result.response'; +import { UpdateUserInput } from './dto/update-user.input'; import graphqlFields from 'graphql-fields'; import type { GraphQLResolveInfo } from 'graphql'; @@ -27,4 +28,10 @@ export class UsersResolver { @Mutation(() => User, { name: 'createUser' }) async createUser(@Args('input') input: CreateUserInput): Promise { return this.usersService.create(input) } + @Mutation(() => User, { name: 'updateUser' }) + async updateUser(@Args('uuid') uuid: string, @Args('input') input: UpdateUserInput): Promise { return this.usersService.update(uuid, input) } + + @Mutation(() => Boolean, { name: 'deleteUser' }) + async deleteUser(@Args('uuid') uuid: string): Promise { return this.usersService.delete(uuid) } + } diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 216ca98..b24c350 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -4,6 +4,7 @@ import { Types, Model } from 'mongoose'; import { User, UserDocument } from '@/models/user.model'; import { CreateUserInput } from './dto/create-user.input'; import { UsersListResponse } from '@/people/dto/list-result.response'; +import { UpdateUserInput } from './dto/update-user.input'; @Injectable() export class UsersService { @@ -13,7 +14,7 @@ export class UsersService { async findAll(projection: any, skip: number, limit: number, sort?: Record, filters?: Record): Promise { const query: any = {}; if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) }; const totalCount = await this.userModel.countDocuments(query).exec(); - const data = await this.userModel.find(query, projection, { lean: true }).skip(skip).limit(limit).sort(sort).exec() + const data = await this.userModel.find(query, projection, { lean: true }).skip(skip).limit(limit).sort(sort).exec(); return { data, totalCount }; } @@ -21,8 +22,10 @@ export class UsersService { async create(input: CreateUserInput): Promise { const user = new this.userModel(input); return user.save() } - buildProjection(fields: Record): Record { - const projection: Record = {}; for (const key in fields) { projection[key] = 1 }; console.dir({ fields, projection }, { depth: null }); return projection - } + async update(uuid: string, input: UpdateUserInput): Promise { const user = await this.userModel.findOne({ uuid }); if (!user) { throw new Error('User not found') }; user.set(input); console.dir({ uuid, input }, { depth: null }); return user.save() } + + async delete(uuid: string): Promise { const user = await this.userModel.deleteMany({ uuid }); return user.deletedCount > 0 } + + buildProjection(fields: Record): Record { const projection: Record = {}; for (const key in fields) { projection[key] = 1 }; return projection } } diff --git a/frontend/app/api/users/add/route.ts b/frontend/app/api/users/add/route.ts new file mode 100644 index 0000000..2907273 --- /dev/null +++ b/frontend/app/api/users/add/route.ts @@ -0,0 +1,53 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { userAddSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + console.log("BODY") + console.dir({ body }) + const validatedBody = userAddSchema.parse(body); + validatedBody.person = "6917732face2287b1d901738" + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + _id + password + tag + email + phone + collectionTokens { + default + tokens { + prefix + token + } + } + person + } + } + `; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + console.log("DATA") + console.dir({ data }) + return NextResponse.json({ data: data.createUser, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +// export async function POST(request: Request) { + +// const body = await request.json(); +// const validatedBody = userAddSchema.parse(body); +// console.log("VALIDATED") +// console.dir({ validatedBody }) +// return NextResponse.json({ data: validatedBody }) +// } diff --git a/frontend/app/api/users/add/schema.ts b/frontend/app/api/users/add/schema.ts new file mode 100644 index 0000000..cb1e0b1 --- /dev/null +++ b/frontend/app/api/users/add/schema.ts @@ -0,0 +1,29 @@ +import { z } from "zod" + +export const tokenSchema = z.object({ + prefix: z.string().min(1, "Prefix is required"), + token: z.string().min(1, "Token is required"), +}) + +export const collectionTokensSchema = z.object({ + default: z.string().optional(), + tokens: z.array(tokenSchema) +}) + +export const userAddSchema = z.object({ + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + + isConfirmed: z.boolean(), + isNotificationSend: z.boolean(), + + password: z.string().min(6), + tag: z.string().optional(), + email: z.string().email(), + phone: z.string().min(5), + person: z.string().optional(), + + collectionTokens: collectionTokensSchema, +}) + +export type UserAdd = z.infer diff --git a/frontend/app/api/users/delete/route.ts b/frontend/app/api/users/delete/route.ts new file mode 100644 index 0000000..fe9acfc --- /dev/null +++ b/frontend/app/api/users/delete/route.ts @@ -0,0 +1,22 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function GET(request: Request) { + + const searchParams = new URL(request.url).searchParams; + const uuid = searchParams.get('uuid'); + if (!uuid) { return NextResponse.json({ error: 'UUID not found in search params' }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql`mutation DeleteUser($uuid: String!) { deleteUser(uuid: $uuid)}`; + const variables = { uuid: uuid }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.deleteUser, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/users/list/route.ts b/frontend/app/api/users/list/route.ts index 9194c8d..856ae10 100644 --- a/frontend/app/api/users/list/route.ts +++ b/frontend/app/api/users/list/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'; import { GraphQLClient, gql } from 'graphql-request'; -const endpoint = process.env.GRAPHQL_URL || "http://localhost:3001/graphql"; +const endpoint = "http://localhost:3001/graphql"; export async function POST(request: Request) { const body = await request.json(); @@ -50,7 +50,6 @@ export async function POST(request: Request) { } } `; - console.dir({ limit, skip, sort, filters }, { depth: null }); const variables = { input: { limit, skip, sort, filters } }; const data = await client.request(query, variables); return NextResponse.json({ data: data.users.data, totalCount: data.users.totalCount }); diff --git a/frontend/app/api/users/update/route.ts b/frontend/app/api/users/update/route.ts new file mode 100644 index 0000000..f374d53 --- /dev/null +++ b/frontend/app/api/users/update/route.ts @@ -0,0 +1,35 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { userUpdateSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const searchUrl = new URL(request.url); + const uuid = searchUrl.searchParams.get("uuid") || ""; + const body = await request.json(); + const validatedBody = userUpdateSchema.parse(body); + if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation UpdateUserTest($uuid: String!, $input: UpdateUserInput!) { + updateUser(uuid: $uuid, input: $input) { + tag + email + phone + person + expiryStarts + expiryEnds + } + } + `; + const variables = { uuid: uuid, input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.updateUser, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/users/update/schema.ts b/frontend/app/api/users/update/schema.ts new file mode 100644 index 0000000..de68b37 --- /dev/null +++ b/frontend/app/api/users/update/schema.ts @@ -0,0 +1,28 @@ +import { z } from "zod" + +export const tokenSchema = z.object({ + prefix: z.string().min(1, "Prefix is required").optional(), + token: z.string().min(1, "Token is required").optional(), +}) + +export const collectionTokensSchema = z.object({ + default: z.string().optional(), + tokens: z.array(tokenSchema).optional() +}) + +export const userUpdateSchema = z.object({ + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + + isConfirmed: z.boolean().optional(), + isNotificationSend: z.boolean().optional(), + + tag: z.string().optional(), + email: z.string().email().optional(), + phone: z.string().min(5).optional(), + person: z.string().optional(), + + collectionTokens: collectionTokensSchema, +}) + +export type UserUpdate = z.infer diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index d829d95..67f2b9c 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,5 +1,6 @@ -import DashboardPage from "@/pages/dashboard/page"; +// import DashboardPage from "@/pages/dashboard/page"; -const PageDashboard = () => { return }; +// const PageDashboard = () => { return }; +const PageDashboard = () => { return <> }; export default PageDashboard; diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 722ee80..6d3858c 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,17 +1,14 @@ +import "./globals.css"; import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { SiteHeader } from "@/components/sidebar/site-header"; import Providers from "./providers"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); +const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", @@ -22,7 +19,21 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac return ( - {children} + + + + + +
+
+
+ {children} +
+
+
+
+
+
); diff --git a/frontend/app/users/add/page.tsx b/frontend/app/users/add/page.tsx new file mode 100644 index 0000000..b494fb3 --- /dev/null +++ b/frontend/app/users/add/page.tsx @@ -0,0 +1,5 @@ +import { PageAddUser } from "@/pages/users/add/page"; + +const AddUserPage = () => { return <> } + +export default AddUserPage; diff --git a/frontend/app/users/update/page.tsx b/frontend/app/users/update/page.tsx new file mode 100644 index 0000000..7cec2cf --- /dev/null +++ b/frontend/app/users/update/page.tsx @@ -0,0 +1,7 @@ +import { PageUpdateUser } from '@/pages/users/update/page'; + +const UpdateUserPage = async () => { + return +} + +export default UpdateUserPage; diff --git a/frontend/components/dashboard/data-table.tsx b/frontend/components/dashboard/data-table.tsx index 54e7ffd..95eb53b 100644 --- a/frontend/components/dashboard/data-table.tsx +++ b/frontend/components/dashboard/data-table.tsx @@ -52,7 +52,6 @@ import { import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" import { toast } from "sonner" import { z } from "zod" - import { useIsMobile } from "@/hooks/use-mobile" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" diff --git a/frontend/components/dashboard/nav-documents.tsx b/frontend/components/dashboard/nav-documents.tsx index 03d5dc6..d1e02ff 100644 --- a/frontend/components/dashboard/nav-documents.tsx +++ b/frontend/components/dashboard/nav-documents.tsx @@ -25,15 +25,7 @@ import { useSidebar, } from "@/components/ui/sidebar" -export function NavDocuments({ - items, -}: { - items: { - name: string - url: string - icon: Icon - }[] -}) { +export function NavDocuments({ items }: { items: { name: string, url: string, icon: Icon }[] }) { const { isMobile } = useSidebar() return ( diff --git a/frontend/components/dashboard/nav-main.tsx b/frontend/components/dashboard/nav-main.tsx index 2f1ef1b..ed73076 100644 --- a/frontend/components/dashboard/nav-main.tsx +++ b/frontend/components/dashboard/nav-main.tsx @@ -10,6 +10,8 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar" +import Link from "next/link" +import { usePathname } from 'next/navigation' export function NavMain({ items, @@ -20,37 +22,30 @@ export function NavMain({ icon?: Icon }[] }) { + const pathname = usePathname() + const linkRenderActive = (item: { title: string; url: string; icon?: Icon }) => + + + + {item.icon && } + {item.title} + + + + + const linkRenderDisabled = (item: { title: string; url: string; icon?: Icon }) => + + + {item.icon && } + {item.title} + + + return ( - - - - Quick Create - - - - - - {items.map((item) => ( - - - {item.icon && } - {item.title} - - - ))} + {items.map((item) => pathname?.includes(item.url) ? linkRenderDisabled(item) : linkRenderActive(item))} diff --git a/frontend/components/dashboard/site-header.tsx b/frontend/components/dashboard/site-header.tsx index f4911ef..337c105 100644 --- a/frontend/components/dashboard/site-header.tsx +++ b/frontend/components/dashboard/site-header.tsx @@ -19,8 +19,7 @@ export function SiteHeader() { rel="noopener noreferrer" target="_blank" className="dark:text-foreground" - > - GitHub + >GitHub diff --git a/frontend/components/sidebar/app-sidebar.tsx b/frontend/components/sidebar/app-sidebar.tsx new file mode 100644 index 0000000..7b44991 --- /dev/null +++ b/frontend/components/sidebar/app-sidebar.tsx @@ -0,0 +1,183 @@ +"use client" + +import * as React from "react" +import { + IconCamera, + IconChartBar, + IconDashboard, + IconDatabase, + IconFileAi, + IconFileDescription, + IconFileWord, + IconFolder, + IconHelp, + IconInnerShadowTop, + IconListDetails, + IconReport, + IconSearch, + IconSettings, + IconUsers, + IconBuilding +} from "@tabler/icons-react" + +import { NavMain } from "@/components/dashboard/nav-main" +import { NavSecondary } from "@/components/dashboard/nav-secondary" +import { NavUser } from "@/components/dashboard/nav-user" +import { NavDocuments } from "@/components/dashboard/nav-documents" + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "https://avatars.githubusercontent.com/u/124599?v=4", + }, + navMain: [ + { + title: "Users", + url: "/users", + icon: IconUsers, + }, + { + title: "People", + url: "/people", + icon: IconListDetails, + }, + { + title: "Build", + url: "/build", + icon: IconBuilding, + }, + // { + // title: "Projects", + // url: "#", + // icon: IconFolder, + // }, + // { + // title: "Team", + // url: "#", + // icon: IconUsers, + // }, + ], + navClouds: [ + { + title: "Capture", + icon: IconCamera, + isActive: true, + url: "#", + items: [ + { + title: "Active Proposals", + url: "#", + }, + { + title: "Archived", + url: "#", + }, + ], + }, + { + title: "Proposal", + icon: IconFileDescription, + url: "#", + items: [ + { + title: "Active Proposals", + url: "#", + }, + { + title: "Archived", + url: "#", + }, + ], + }, + { + title: "Prompts", + icon: IconFileAi, + url: "#", + items: [ + { + title: "Active Proposals", + url: "#", + }, + { + title: "Archived", + url: "#", + }, + ], + }, + ], + navSecondary: [ + { + title: "Settings", + url: "#", + icon: IconSettings, + }, + { + title: "Get Help", + url: "#", + icon: IconHelp, + }, + { + title: "Search", + url: "#", + icon: IconSearch, + }, + ], + documents: [ + { + name: "Data Library", + url: "#", + icon: IconDatabase, + }, + { + name: "Reports", + url: "#", + icon: IconReport, + }, + { + name: "Word Assistant", + url: "#", + icon: IconFileWord, + }, + ], +} + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + + + Acme Inc. + + + + + + + + + + + + + + + ) +} diff --git a/frontend/components/sidebar/navs/nav-documents.tsx b/frontend/components/sidebar/navs/nav-documents.tsx new file mode 100644 index 0000000..03d5dc6 --- /dev/null +++ b/frontend/components/sidebar/navs/nav-documents.tsx @@ -0,0 +1,92 @@ +"use client" + +import { + IconDots, + IconFolder, + IconShare3, + IconTrash, + type Icon, +} from "@tabler/icons-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function NavDocuments({ + items, +}: { + items: { + name: string + url: string + icon: Icon + }[] +}) { + const { isMobile } = useSidebar() + + return ( + + Documents + + {items.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + Open + + + + Share + + + + + Delete + + + + + ))} + + + + More + + + + + ) +} diff --git a/frontend/components/sidebar/navs/nav-main.tsx b/frontend/components/sidebar/navs/nav-main.tsx new file mode 100644 index 0000000..2f1ef1b --- /dev/null +++ b/frontend/components/sidebar/navs/nav-main.tsx @@ -0,0 +1,58 @@ +"use client" + +import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react" + +import { Button } from "@/components/ui/button" +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavMain({ + items, +}: { + items: { + title: string + url: string + icon?: Icon + }[] +}) { + return ( + + + + + + + Quick Create + + + + + + {items.map((item) => ( + + + {item.icon && } + {item.title} + + + ))} + + + + ) +} diff --git a/frontend/components/sidebar/navs/nav-secondary.tsx b/frontend/components/sidebar/navs/nav-secondary.tsx new file mode 100644 index 0000000..7ee5341 --- /dev/null +++ b/frontend/components/sidebar/navs/nav-secondary.tsx @@ -0,0 +1,42 @@ +"use client" + +import * as React from "react" +import { type Icon } from "@tabler/icons-react" + +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavSecondary({ + items, + ...props +}: { + items: { + title: string + url: string + icon: Icon + }[] +} & React.ComponentPropsWithoutRef) { + return ( + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ) +} diff --git a/frontend/components/sidebar/navs/nav-user.tsx b/frontend/components/sidebar/navs/nav-user.tsx new file mode 100644 index 0000000..cf8ee7a --- /dev/null +++ b/frontend/components/sidebar/navs/nav-user.tsx @@ -0,0 +1,110 @@ +"use client" + +import { + IconCreditCard, + IconDotsVertical, + IconLogout, + IconNotification, + IconUserCircle, +} from "@tabler/icons-react" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +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} + +
+
+
+ + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ) +} diff --git a/frontend/components/sidebar/site-header.tsx b/frontend/components/sidebar/site-header.tsx new file mode 100644 index 0000000..337c105 --- /dev/null +++ b/frontend/components/sidebar/site-header.tsx @@ -0,0 +1,29 @@ +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" +import { SidebarTrigger } from "@/components/ui/sidebar" + +export function SiteHeader() { + return ( +
+
+ + +

Documents

+
+ +
+
+
+ ) +} diff --git a/frontend/components/ui/calendar.tsx b/frontend/components/ui/calendar.tsx new file mode 100644 index 0000000..7fc9dc8 --- /dev/null +++ b/frontend/components/ui/calendar.tsx @@ -0,0 +1,216 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( + + + + + {/* Calendar */} + + + {/* Inputs */} +
+ handleInput("d", e.target.value)} + onBlur={() => handleBlur("d")} + /> + handleInput("m", e.target.value)} + onBlur={() => handleBlur("m")} + /> + handleInput("y", e.target.value)} + onBlur={() => handleBlur("y")} + /> +
+ +
+ handleInput("h", e.target.value)} + onBlur={() => handleBlur("h")} + /> + handleInput("min", e.target.value)} + onBlur={() => handleBlur("min")} + /> + handleInput("s", e.target.value)} + onBlur={() => handleBlur("s")} + /> +
+
+ + ); +} + +// --- helpers for external usage --- +export const dateGetter = (str: string | undefined): Date | null => { + if (!str) return null; + const [d, m, yAndTime] = str.split("/"); + if (!d || !m || !yAndTime) return null; + const [y, time] = yAndTime.split(" "); + if (!time) return null; + const [h, min, s] = time.split(":").map(Number); + return new Date(Number(y), Number(m) - 1, Number(d), h || 0, min || 0, s || 0); +}; + +export const dateSetter = (date: Date | null): string => { + if (!date) return ""; + const fmt = (n: number) => String(n).padStart(2, "0"); + return `${fmt(date.getDate())}/${fmt(date.getMonth() + 1)}/${date.getFullYear()} ${fmt( + date.getHours() + )}:${fmt(date.getMinutes())}:${fmt(date.getSeconds())}`; +}; diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx index bbe6fb0..b56470a 100644 --- a/frontend/components/ui/dropdown-menu.tsx +++ b/frontend/components/ui/dropdown-menu.tsx @@ -42,7 +42,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", className )} {...props} @@ -74,7 +74,7 @@ function DropdownMenuItem({ data-inset={inset} data-variant={variant} className={cn( - "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className )} {...props} @@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({ = 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 } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + 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 +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +