diff options
| author | Dhravya <[email protected]> | 2024-06-14 23:35:19 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-06-14 23:35:19 -0500 |
| commit | 9f0fb14d56fcbd9280aee2bdd83d55e8fabbe7d2 (patch) | |
| tree | 372de593c837b0a963c081a1db58447ac4e08084 | |
| parent | Merge branch 'codetorso' of https://github.com/Dhravya/supermemory into codet... (diff) | |
| download | supermemory-9f0fb14d56fcbd9280aee2bdd83d55e8fabbe7d2.tar.xz supermemory-9f0fb14d56fcbd9280aee2bdd83d55e8fabbe7d2.zip | |
[MIGRATION REQUIRED]Data fetchers and other server actions, spaces creation and database migrations
| -rw-r--r-- | apps/web/app/(dash)/actions.ts | 48 | ||||
| -rw-r--r-- | apps/web/app/(dash)/dynamicisland.tsx | 109 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/page.tsx | 11 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/queryinput.tsx | 21 | ||||
| -rw-r--r-- | apps/web/app/(dash)/layout.tsx | 7 | ||||
| -rw-r--r-- | apps/web/app/actions/doers.ts | 43 | ||||
| -rw-r--r-- | apps/web/app/actions/fetchers.ts | 25 | ||||
| -rw-r--r-- | apps/web/app/actions/types.ts | 10 | ||||
| -rw-r--r-- | apps/web/app/helpers/server/auth.ts | 25 | ||||
| -rw-r--r-- | apps/web/app/helpers/server/db/schema.ts | 99 | ||||
| -rw-r--r-- | apps/web/migrations/000_setup.sql | 55 | ||||
| -rw-r--r-- | apps/web/migrations/meta/0000_snapshot.json | 158 | ||||
| -rw-r--r-- | apps/web/migrations/meta/_journal.json | 4 | ||||
| -rw-r--r-- | apps/web/package.json | 3 | ||||
| -rw-r--r-- | package.json | 4 | ||||
| -rw-r--r-- | packages/ui/shadcn/select.tsx | 160 | ||||
| -rw-r--r-- | packages/ui/shadcn/sonner.tsx | 31 |
17 files changed, 599 insertions, 214 deletions
diff --git a/apps/web/app/(dash)/actions.ts b/apps/web/app/(dash)/actions.ts deleted file mode 100644 index 70c2a567..00000000 --- a/apps/web/app/(dash)/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -"use server"; - -import { cookies, headers } from "next/headers"; -import { db } from "../helpers/server/db"; -import { sessions, users, space } from "../helpers/server/db/schema"; -import { eq } from "drizzle-orm"; -import { redirect } from "next/navigation"; - -export async function ensureAuth() { - const token = - cookies().get("next-auth.session-token")?.value ?? - cookies().get("__Secure-authjs.session-token")?.value ?? - cookies().get("authjs.session-token")?.value ?? - headers().get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return undefined; - } - - const sessionData = await db - .select() - .from(sessions) - .innerJoin(users, eq(users.id, sessions.userId)) - .where(eq(sessions.sessionToken, token)); - - if (!sessionData || sessionData.length < 0) { - return undefined; - } - - return { - user: sessionData[0]!.user, - session: sessionData[0]!, - }; -} - -export async function getSpaces() { - const data = await ensureAuth(); - if (!data) { - redirect("/signin"); - } - - const sp = await db - .select() - .from(space) - .where(eq(space.user, data.user.email)); - - return sp; -} diff --git a/apps/web/app/(dash)/dynamicisland.tsx b/apps/web/app/(dash)/dynamicisland.tsx index b703d55a..31f76fda 100644 --- a/apps/web/app/(dash)/dynamicisland.tsx +++ b/apps/web/app/(dash)/dynamicisland.tsx @@ -9,6 +9,17 @@ import { motion } from "framer-motion"; import { Label } from "@repo/ui/shadcn/label"; import { Input } from "@repo/ui/shadcn/input"; import { Textarea } from "@repo/ui/shadcn/textarea"; +import { createSpace } from "../actions/doers"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/ui/shadcn/select"; +import { Space } from "../actions/types"; +import { getSpaces } from "../actions/fetchers"; +import { toast } from "sonner"; export function DynamicIsland() { const { scrollYProgress } = useScroll(); @@ -80,7 +91,7 @@ function DynamicIslandContent() { {show ? ( <div onClick={() => setshow(!show)} - className="bg-[#1F2428] px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer" + className="bg-secondary px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer" > <div className="flex gap-4 items-center"> <Image src={AddIcon} alt="Add icon" /> @@ -99,7 +110,25 @@ function DynamicIslandContent() { const fakeitems = ["spaces", "page", "note"]; function ToolBar({ cancelfn }: { cancelfn: () => void }) { + const [spaces, setSpaces] = useState<Space[]>([]); + const [index, setIndex] = useState(0); + + useEffect(() => { + (async () => { + let spaces = await getSpaces(); + + if (!spaces.success || !spaces.data) { + toast.warning("Unable to get spaces", { + richColors: true, + }); + setSpaces([]); + return; + } + setSpaces(spaces.data); + })(); + }, []); + return ( <AnimatePresence mode="wait"> <motion.div @@ -120,7 +149,7 @@ function ToolBar({ cancelfn }: { cancelfn: () => void }) { }} className="flex flex-col items-center" > - <div className="bg-[#1F2428] py-[.35rem] px-[.6rem] rounded-2xl"> + <div className="bg-secondary py-[.35rem] px-[.6rem] rounded-2xl"> <HoverEffect items={fakeitems} index={index} @@ -130,9 +159,9 @@ function ToolBar({ cancelfn }: { cancelfn: () => void }) { {index === 0 ? ( <SpaceForm cancelfn={cancelfn} /> ) : index === 1 ? ( - <PageForm cancelfn={cancelfn} /> + <PageForm cancelfn={cancelfn} spaces={spaces} /> ) : ( - <NoteForm cancelfn={cancelfn} /> + <NoteForm cancelfn={cancelfn} spaces={spaces} /> )} </motion.div> </AnimatePresence> @@ -182,7 +211,10 @@ export const HoverEffect = ({ function SpaceForm({ cancelfn }: { cancelfn: () => void }) { return ( - <div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> + <form + action={createSpace} + className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3" + > <div> <Label className="text-[#858B92]" htmlFor="name"> Name @@ -190,34 +222,55 @@ function SpaceForm({ cancelfn }: { cancelfn: () => void }) { <Input className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0" id="name" + name="name" /> </div> <div className="flex justify-between"> <a className="text-blue-500" href=""> pull from store </a> - <div + {/* <div onClick={cancelfn} className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer" > cancel - </div> + </div> */} + <button + type="submit" + className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer" + > + Submit + </button> </div> - </div> + </form> ); } -function PageForm({ cancelfn }: { cancelfn: () => void }) { +function PageForm({ + cancelfn, + spaces, +}: { + cancelfn: () => void; + spaces: Space[]; +}) { return ( - <div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> + <div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> <div> - <Label className="text-[#858B92]" htmlFor="name"> + <Label className="text-[#858B92]" htmlFor="space"> Space </Label> - <Input - className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0" - id="name" - /> + <Select> + <SelectTrigger> + <SelectValue placeholder="Space" /> + </SelectTrigger> + <SelectContent className="bg-secondary text-white"> + {spaces.map((space) => ( + <SelectItem key={space.id} value={space.id.toString()}> + {space.name} + </SelectItem> + ))} + </SelectContent> + </Select> </div> <div> <Label className="text-[#858B92]" htmlFor="name"> @@ -240,17 +293,31 @@ function PageForm({ cancelfn }: { cancelfn: () => void }) { ); } -function NoteForm({ cancelfn }: { cancelfn: () => void }) { +function NoteForm({ + cancelfn, + spaces, +}: { + cancelfn: () => void; + spaces: Space[]; +}) { return ( - <div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> + <div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> <div> <Label className="text-[#858B92]" htmlFor="name"> Space </Label> - <Input - className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0" - id="name" - /> + <Select> + <SelectTrigger> + <SelectValue placeholder="Space" /> + </SelectTrigger> + <SelectContent className="bg-secondary text-white"> + {spaces.map((space) => ( + <SelectItem key={space.id} value={space.id.toString()}> + {space.name} + </SelectItem> + ))} + </SelectContent> + </Select> </div> <div> <Label className="text-[#858B92]" htmlFor="name"> diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx index 0c75e457..b4bafb38 100644 --- a/apps/web/app/(dash)/home/page.tsx +++ b/apps/web/app/(dash)/home/page.tsx @@ -3,7 +3,7 @@ import Menu from "../menu"; import Header from "../header"; import QueryInput from "./queryinput"; import { homeSearchParamsCache } from "@/app/helpers/lib/searchParams"; -import { getSpaces } from "../actions"; +import { getSpaces } from "@/app/actions/fetchers"; async function Page({ searchParams, @@ -13,7 +13,12 @@ async function Page({ // TODO: use this to show a welcome page/modal const { firstTime } = homeSearchParamsCache.parse(searchParams); - const spaces = await getSpaces(); + let spaces = await getSpaces(); + + if (!spaces.success) { + // TODO: handle this error properly. + spaces.data = []; + } return ( <div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col"> @@ -21,7 +26,7 @@ async function Page({ {/* <div className="">hi {firstTime ? 'first time' : ''}</div> */} <div className="w-full h-96"> - <QueryInput initialSpaces={spaces} /> + <QueryInput initialSpaces={spaces.data} /> </div> </div> ); diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx index 4cb1fdb2..d0c27b8d 100644 --- a/apps/web/app/(dash)/home/queryinput.tsx +++ b/apps/web/app/(dash)/home/queryinput.tsx @@ -2,10 +2,11 @@ import { ArrowRightIcon } from "@repo/ui/icons"; import Image from "next/image"; -import React, { useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import Divider from "@repo/ui/shadcn/divider"; import { MultipleSelector, Option } from "@repo/ui/shadcn/combobox"; import { useRouter } from "next/navigation"; +import { getSpaces } from "@/app/actions/fetchers"; function QueryInput({ initialQuery = "", @@ -13,7 +14,10 @@ function QueryInput({ disabled = false, }: { initialQuery?: string; - initialSpaces?: { user: string | null; id: number; name: string }[]; + initialSpaces?: { + id: number; + name: string; + }[]; disabled?: boolean; }) { const [q, setQ] = useState(initialQuery); @@ -41,10 +45,14 @@ function QueryInput({ return newQ; }; - const options = initialSpaces.map((x) => ({ - label: x.name, - value: x.id.toString(), - })); + const options = useMemo( + () => + initialSpaces.map((x) => ({ + label: x.name, + value: x.id.toString(), + })), + [initialSpaces], + ); return ( <div> @@ -82,6 +90,7 @@ function QueryInput({ {/* selected sources */} <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]"> <MultipleSelector + key={options.length} disabled={disabled} defaultOptions={options} onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))} diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx index 85f8476e..b879a2f5 100644 --- a/apps/web/app/(dash)/layout.tsx +++ b/apps/web/app/(dash)/layout.tsx @@ -1,10 +1,11 @@ import Header from "./header"; import Menu from "./menu"; -import { ensureAuth } from "./actions"; import { redirect } from "next/navigation"; +import { auth } from "../helpers/server/auth"; +import { Toaster } from "@repo/ui/shadcn/sonner"; async function Layout({ children }: { children: React.ReactNode }) { - const info = await ensureAuth(); + const info = await auth(); if (!info) { return redirect("/signin"); @@ -17,6 +18,8 @@ async function Layout({ children }: { children: React.ReactNode }) { <Menu /> {children} + + <Toaster /> </main> ); } diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts new file mode 100644 index 00000000..c8a1f3b4 --- /dev/null +++ b/apps/web/app/actions/doers.ts @@ -0,0 +1,43 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { db } from "../helpers/server/db"; +import { space } from "../helpers/server/db/schema"; +import { ServerActionReturnType } from "./types"; +import { auth } from "../helpers/server/auth"; + +export const createSpace = async ( + input: string | FormData, +): ServerActionReturnType<number> => { + const data = await auth(); + + if (!data || !data.user) { + return { error: "Not authenticated", success: false }; + } + + if (typeof input === "object") { + input = (input as FormData).get("name") as string; + } + + try { + const resp = await db + .insert(space) + .values({ name: input, user: data.user.id }); + + revalidatePath("/home"); + return { success: true, data: 1 }; + } catch (e: unknown) { + const error = e as Error; + if ( + error.message.includes("D1_ERROR: UNIQUE constraint failed: space.name") + ) { + return { success: false, data: 0, error: "Space already exists" }; + } else { + return { + success: false, + data: 0, + error: "Failed to create space with error: " + error.message, + }; + } + } +}; diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts new file mode 100644 index 00000000..9c2527f0 --- /dev/null +++ b/apps/web/app/actions/fetchers.ts @@ -0,0 +1,25 @@ +"use server"; + +import { eq } from "drizzle-orm"; +import { db } from "../helpers/server/db"; +import { users } from "../helpers/server/db/schema"; +import { ServerActionReturnType, Space } from "./types"; +import { auth } from "../helpers/server/auth"; + +export const getSpaces = async (): ServerActionReturnType<Space[]> => { + const data = await auth(); + + if (!data || !data.user) { + return { error: "Not authenticated", success: false }; + } + + const spaces = await db.query.space.findMany({ + where: eq(users, data.user.id), + }); + + const spacesWithoutUser = spaces.map((space) => { + return { ...space, user: undefined }; + }); + + return { success: true, data: spacesWithoutUser }; +}; diff --git a/apps/web/app/actions/types.ts b/apps/web/app/actions/types.ts new file mode 100644 index 00000000..fbf669e2 --- /dev/null +++ b/apps/web/app/actions/types.ts @@ -0,0 +1,10 @@ +export type Space = { + id: number; + name: string; +}; + +export type ServerActionReturnType<T> = Promise<{ + error?: string; + success: boolean; + data?: T; +}>; diff --git a/apps/web/app/helpers/server/auth.ts b/apps/web/app/helpers/server/auth.ts index 73119d87..c4e426d4 100644 --- a/apps/web/app/helpers/server/auth.ts +++ b/apps/web/app/helpers/server/auth.ts @@ -2,6 +2,7 @@ import NextAuth, { NextAuthResult } from "next-auth"; import Google from "next-auth/providers/google"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { db } from "./db"; +import { accounts, sessions, users, verificationTokens } from "./db/schema"; export const { handlers: { GET, POST }, @@ -10,16 +11,20 @@ export const { auth, } = NextAuth({ secret: process.env.BACKEND_SECURITY_KEY, - callbacks: { - session: ({ session, token, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: DrizzleAdapter(db), + // callbacks: { + // session: ({ session, token, user }) => ({ + // ...session, + // user: { + // ...session.user, + // }, + // }), + // }, + adapter: DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, + }), providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID, diff --git a/apps/web/app/helpers/server/db/schema.ts b/apps/web/app/helpers/server/db/schema.ts index c4616eb2..e3e789c6 100644 --- a/apps/web/app/helpers/server/db/schema.ts +++ b/apps/web/app/helpers/server/db/schema.ts @@ -7,75 +7,88 @@ import { text, integer, } from "drizzle-orm/sqlite-core"; +import type { AdapterAccountType } from "next-auth/adapters"; export const createTable = sqliteTableCreator((name) => `${name}`); export const users = createTable("user", { - id: text("id", { length: 255 }).notNull().primaryKey(), - name: text("name", { length: 255 }), - email: text("email", { length: 255 }).notNull(), - emailVerified: int("emailVerified", { mode: "timestamp" }).default( - sql`CURRENT_TIMESTAMP`, - ), - image: text("image", { length: 255 }), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: text("name"), + email: text("email").notNull(), + emailVerified: integer("emailVerified", { mode: "timestamp_ms" }), + image: text("image"), }); export type User = typeof users.$inferSelect; -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - export const accounts = createTable( "account", { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - userId: text("userId", { length: 255 }) + userId: text("userId") .notNull() .references(() => users.id, { onDelete: "cascade" }), - type: text("type", { length: 255 }).notNull(), - provider: text("provider", { length: 255 }).notNull(), - providerAccountId: text("providerAccountId", { length: 255 }).notNull(), + type: text("type").$type<AdapterAccountType>().notNull(), + provider: text("provider").notNull(), + providerAccountId: text("providerAccountId").notNull(), refresh_token: text("refresh_token"), access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: text("token_type", { length: 255 }), - scope: text("scope", { length: 255 }), + expires_at: integer("expires_at"), + token_type: text("token_type"), + scope: text("scope"), id_token: text("id_token"), - session_state: text("session_state", { length: 255 }), - oauth_token_secret: text("oauth_token_secret"), - oauth_token: text("oauth_token"), + session_state: text("session_state"), }, (account) => ({ - userIdIdx: index("account_userId_idx").on(account.userId), + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), }), ); -export const sessions = createTable( - "session", +export const sessions = createTable("session", { + sessionToken: text("sessionToken").primaryKey(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), +}); + +export const verificationTokens = createTable( + "verificationToken", { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - sessionToken: text("sessionToken", { length: 255 }).notNull(), - userId: text("userId", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - expires: int("expires", { mode: "timestamp" }).notNull(), + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), }, - (session) => ({ - userIdIdx: index("session_userId_idx").on(session.userId), + (verificationToken) => ({ + compositePk: primaryKey({ + columns: [verificationToken.identifier, verificationToken.token], + }), }), ); -export const verificationTokens = createTable( - "verificationToken", +export const authenticators = createTable( + "authenticator", { - identifier: text("identifier", { length: 255 }).notNull(), - token: text("token", { length: 255 }).notNull(), - expires: int("expires", { mode: "timestamp" }).notNull(), + credentialID: text("credentialID").notNull().unique(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + providerAccountId: text("providerAccountId").notNull(), + credentialPublicKey: text("credentialPublicKey").notNull(), + counter: integer("counter").notNull(), + credentialDeviceType: text("credentialDeviceType").notNull(), + credentialBackedUp: integer("credentialBackedUp", { + mode: "boolean", + }).notNull(), + transports: text("transports"), }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + (authenticator) => ({ + compositePK: primaryKey({ + columns: [authenticator.userId, authenticator.credentialID], + }), }), ); @@ -94,7 +107,7 @@ export const storedContent = createTable( "page", ), image: text("image", { length: 255 }), - user: text("user", { length: 255 }).references(() => users.id, { + userId: int("user").references(() => users.id, { onDelete: "cascade", }), }, @@ -102,7 +115,7 @@ export const storedContent = createTable( urlIdx: index("storedContent_url_idx").on(sc.url), savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt), titleInx: index("storedContent_title_idx").on(sc.title), - userIdx: index("storedContent_user_idx").on(sc.user), + userIdx: index("storedContent_user_idx").on(sc.userId), }), ); diff --git a/apps/web/migrations/000_setup.sql b/apps/web/migrations/000_setup.sql index db7f9444..0c151b98 100644 --- a/apps/web/migrations/000_setup.sql +++ b/apps/web/migrations/000_setup.sql @@ -1,18 +1,29 @@ CREATE TABLE `account` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `userId` text(255) NOT NULL, - `type` text(255) NOT NULL, - `provider` text(255) NOT NULL, - `providerAccountId` text(255) NOT NULL, + `userId` text NOT NULL, + `type` text NOT NULL, + `provider` text NOT NULL, + `providerAccountId` text NOT NULL, `refresh_token` text, `access_token` text, `expires_at` integer, - `token_type` text(255), - `scope` text(255), + `token_type` text, + `scope` text, `id_token` text, - `session_state` text(255), - `oauth_token_secret` text, - `oauth_token` text, + `session_state` text, + PRIMARY KEY(`provider`, `providerAccountId`), + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `authenticator` ( + `credentialID` text NOT NULL, + `userId` text NOT NULL, + `providerAccountId` text NOT NULL, + `credentialPublicKey` text NOT NULL, + `counter` integer NOT NULL, + `credentialDeviceType` text NOT NULL, + `credentialBackedUp` integer NOT NULL, + `transports` text, + PRIMARY KEY(`credentialID`, `userId`), FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint @@ -25,9 +36,8 @@ CREATE TABLE `contentToSpace` ( ); --> statement-breakpoint CREATE TABLE `session` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `sessionToken` text(255) NOT NULL, - `userId` text(255) NOT NULL, + `sessionToken` text PRIMARY KEY NOT NULL, + `userId` text NOT NULL, `expires` integer NOT NULL, FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); @@ -50,27 +60,26 @@ CREATE TABLE `storedContent` ( `ogImage` text(255), `type` text DEFAULT 'page', `image` text(255), - `user` text(255), + `user` integer, FOREIGN KEY (`user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE TABLE `user` ( - `id` text(255) PRIMARY KEY NOT NULL, - `name` text(255), - `email` text(255) NOT NULL, - `emailVerified` integer DEFAULT CURRENT_TIMESTAMP, - `image` text(255) + `id` text PRIMARY KEY NOT NULL, + `name` text, + `email` text NOT NULL, + `emailVerified` integer, + `image` text ); --> statement-breakpoint CREATE TABLE `verificationToken` ( - `identifier` text(255) NOT NULL, - `token` text(255) NOT NULL, + `identifier` text NOT NULL, + `token` text NOT NULL, `expires` integer NOT NULL, PRIMARY KEY(`identifier`, `token`) ); --> statement-breakpoint -CREATE INDEX `account_userId_idx` ON `account` (`userId`);--> statement-breakpoint -CREATE INDEX `session_userId_idx` ON `session` (`userId`);--> statement-breakpoint +CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);--> statement-breakpoint CREATE UNIQUE INDEX `space_name_unique` ON `space` (`name`);--> statement-breakpoint CREATE INDEX `spaces_name_idx` ON `space` (`name`);--> statement-breakpoint CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json index 29cc4323..20327dda 100644 --- a/apps/web/migrations/meta/0000_snapshot.json +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -1,43 +1,36 @@ { "version": "6", "dialect": "sqlite", - "id": "409cec60-0c4b-4cda-8751-3e70768bbb6c", + "id": "4a568d9b-a0e6-44ed-946b-694e34b063f3", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { "name": "account", "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, "userId": { "name": "userId", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "type": { "name": "type", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "provider": { "name": "provider", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "providerAccountId": { "name": "providerAccountId", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false @@ -65,14 +58,14 @@ }, "token_type": { "name": "token_type", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "scope": { "name": "scope", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false @@ -86,20 +79,86 @@ }, "session_state": { "name": "session_state", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": ["provider", "providerAccountId"], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "authenticator": { + "name": "authenticator", + "columns": { + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false }, - "oauth_token_secret": { - "name": "oauth_token_secret", + "userId": { + "name": "userId", "type": "text", "primaryKey": false, - "notNull": false, + "notNull": true, "autoincrement": false }, - "oauth_token": { - "name": "oauth_token", + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentialPublicKey": { + "name": "credentialPublicKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentialDeviceType": { + "name": "credentialDeviceType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentialBackedUp": { + "name": "credentialBackedUp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", "type": "text", "primaryKey": false, "notNull": false, @@ -107,16 +166,16 @@ } }, "indexes": { - "account_userId_idx": { - "name": "account_userId_idx", - "columns": ["userId"], - "isUnique": false + "authenticator_credentialID_unique": { + "name": "authenticator_credentialID_unique", + "columns": ["credentialID"], + "isUnique": true } }, "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", + "authenticator_userId_user_id_fk": { + "name": "authenticator_userId_user_id_fk", + "tableFrom": "authenticator", "tableTo": "user", "columnsFrom": ["userId"], "columnsTo": ["id"], @@ -124,7 +183,12 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "authenticator_userId_credentialID_pk": { + "columns": ["credentialID", "userId"], + "name": "authenticator_userId_credentialID_pk" + } + }, "uniqueConstraints": {} }, "contentToSpace": { @@ -177,23 +241,16 @@ "session": { "name": "session", "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, "sessionToken": { "name": "sessionToken", - "type": "text(255)", - "primaryKey": false, + "type": "text", + "primaryKey": true, "notNull": true, "autoincrement": false }, "userId": { "name": "userId", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false @@ -206,13 +263,7 @@ "autoincrement": false } }, - "indexes": { - "session_userId_idx": { - "name": "session_userId_idx", - "columns": ["userId"], - "isUnique": false - } - }, + "indexes": {}, "foreignKeys": { "session_userId_user_id_fk": { "name": "session_userId_user_id_fk", @@ -360,7 +411,7 @@ }, "user": { "name": "user", - "type": "text(255)", + "type": "integer", "primaryKey": false, "notNull": false, "autoincrement": false @@ -407,21 +458,21 @@ "columns": { "id": { "name": "id", - "type": "text(255)", + "type": "text", "primaryKey": true, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "email": { "name": "email", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false @@ -431,12 +482,11 @@ "type": "integer", "primaryKey": false, "notNull": false, - "autoincrement": false, - "default": "CURRENT_TIMESTAMP" + "autoincrement": false }, "image": { "name": "image", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false @@ -452,14 +502,14 @@ "columns": { "identifier": { "name": "identifier", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "token": { "name": "token", - "type": "text(255)", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index a77d9616..90bb9df7 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1716677954608, - "tag": "0000_calm_monster_badoon", + "when": 1718412145023, + "tag": "0000_absurd_pandemic", "breakpoints": true } ] diff --git a/apps/web/package.json b/apps/web/package.json index 324f5655..19f77187 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,7 +11,8 @@ "pages:build": "bunx @cloudflare/next-on-pages", "preview": "bun pages:build && wrangler pages dev", "deploy": "bun pages:build && wrangler pages deploy", - "schema-update": "bunx drizzle-orm" + "schema-update": "bunx drizzle-kit generate sqlite", + "update-local-db": "bunx wrangler d1 execute dev-d1-anycontext --local" }, "dependencies": { "@million/lint": "^1.0.0-rc.11", diff --git a/package.json b/package.json index bcf4b42c..3c9f4621 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", @@ -73,6 +74,7 @@ "lucide-react": "^0.379.0", "next-app-theme": "^0.1.10", "next-auth": "^5.0.0-beta.18", + "next-themes": "^0.3.0", "random-js": "^2.1.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.51.5", @@ -81,7 +83,7 @@ "rehype-katex": "^7.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", - "sonner": "^1.4.41", + "sonner": "^1.5.0", "tailwind-scrollbar": "^3.1.0", "tldraw": "^2.1.4", "uploadthing": "^6.10.4", diff --git a/packages/ui/shadcn/select.tsx b/packages/ui/shadcn/select.tsx new file mode 100644 index 00000000..8abe27c1 --- /dev/null +++ b/packages/ui/shadcn/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@repo/ui/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> +>(({ className, children, ...props }, ref) => ( + <SelectPrimitive.Trigger + ref={ref} + className={cn( + "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", + className, + )} + {...props} + > + {children} + <SelectPrimitive.Icon asChild> + <ChevronDown className="h-4 w-4 opacity-50" /> + </SelectPrimitive.Icon> + </SelectPrimitive.Trigger> +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.ScrollUpButton + ref={ref} + className={cn( + "flex cursor-default items-center justify-center py-1", + className, + )} + {...props} + > + <ChevronUp className="h-4 w-4" /> + </SelectPrimitive.ScrollUpButton> +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.ScrollDownButton + ref={ref} + className={cn( + "flex cursor-default items-center justify-center py-1", + className, + )} + {...props} + > + <ChevronDown className="h-4 w-4" /> + </SelectPrimitive.ScrollDownButton> +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> +>(({ className, children, position = "popper", ...props }, ref) => ( + <SelectPrimitive.Portal> + <SelectPrimitive.Content + ref={ref} + className={cn( + "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md text-popover-foreground shadow-md 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", + position === "popper" && + "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", + className, + )} + position={position} + {...props} + > + <SelectScrollUpButton /> + <SelectPrimitive.Viewport + className={cn( + "p-1", + position === "popper" && + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", + )} + > + {children} + </SelectPrimitive.Viewport> + <SelectScrollDownButton /> + </SelectPrimitive.Content> + </SelectPrimitive.Portal> +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.Label + ref={ref} + className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} + {...props} + /> +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> +>(({ className, children, ...props }, ref) => ( + <SelectPrimitive.Item + ref={ref} + className={cn( + "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-foreground-menu data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className, + )} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <SelectPrimitive.ItemIndicator> + <Check className="h-4 w-4" /> + </SelectPrimitive.ItemIndicator> + </span> + + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> + </SelectPrimitive.Item> +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.Separator + ref={ref} + className={cn("-mx-1 my-1 h-px bg-muted", className)} + {...props} + /> +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/packages/ui/shadcn/sonner.tsx b/packages/ui/shadcn/sonner.tsx new file mode 100644 index 00000000..549cf841 --- /dev/null +++ b/packages/ui/shadcn/sonner.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps<typeof Sonner>; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + <Sonner + theme={theme as ToasterProps["theme"]} + className="toaster group" + toastOptions={{ + classNames: { + toast: + "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", + description: "group-[.toast]:text-muted-foreground", + actionButton: + "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", + cancelButton: + "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", + }, + }} + {...props} + /> + ); +}; + +export { Toaster }; |