mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-06 08:16:03 +00:00
[MIGRATION REQUIRED]Data fetchers and other server actions, spaces creation and database migrations
This commit is contained in:
parent
ac3b161781
commit
9f0fb14d56
17 changed files with 609 additions and 224 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
43
apps/web/app/actions/doers.ts
Normal file
43
apps/web/app/actions/doers.ts
Normal file
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
25
apps/web/app/actions/fetchers.ts
Normal file
25
apps/web/app/actions/fetchers.ts
Normal file
|
|
@ -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 };
|
||||
};
|
||||
10
apps/web/app/actions/types.ts
Normal file
10
apps/web/app/actions/types.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export type Space = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ServerActionReturnType<T> = Promise<{
|
||||
error?: string;
|
||||
success: boolean;
|
||||
data?: T;
|
||||
}>;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
{
|
||||
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(),
|
||||
},
|
||||
(session) => ({
|
||||
userIdIdx: index("session_userId_idx").on(session.userId),
|
||||
}),
|
||||
);
|
||||
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",
|
||||
{
|
||||
identifier: text("identifier", { length: 255 }).notNull(),
|
||||
token: text("token", { length: 255 }).notNull(),
|
||||
expires: int("expires", { mode: "timestamp" }).notNull(),
|
||||
identifier: text("identifier").notNull(),
|
||||
token: text("token").notNull(),
|
||||
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||
},
|
||||
(vt) => ({
|
||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||
(verificationToken) => ({
|
||||
compositePk: primaryKey({
|
||||
columns: [verificationToken.identifier, verificationToken.token],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const authenticators = createTable(
|
||||
"authenticator",
|
||||
{
|
||||
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"),
|
||||
},
|
||||
(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),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,33 +79,13 @@
|
|||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "text(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"oauth_token_secret": {
|
||||
"name": "oauth_token_secret",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"oauth_token": {
|
||||
"name": "oauth_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_userId_idx": {
|
||||
"name": "account_userId_idx",
|
||||
"columns": ["userId"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_userId_user_id_fk": {
|
||||
"name": "account_userId_user_id_fk",
|
||||
|
|
@ -124,7 +97,98 @@
|
|||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"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
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"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,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"authenticator_credentialID_unique": {
|
||||
"name": "authenticator_credentialID_unique",
|
||||
"columns": ["credentialID"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"authenticator_userId_user_id_fk": {
|
||||
"name": "authenticator_userId_user_id_fk",
|
||||
"tableFrom": "authenticator",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["userId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1716677954608,
|
||||
"tag": "0000_calm_monster_badoon",
|
||||
"when": 1718412145023,
|
||||
"tag": "0000_absurd_pandemic",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
160
packages/ui/shadcn/select.tsx
Normal file
160
packages/ui/shadcn/select.tsx
Normal file
|
|
@ -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,
|
||||
};
|
||||
31
packages/ui/shadcn/sonner.tsx
Normal file
31
packages/ui/shadcn/sonner.tsx
Normal file
|
|
@ -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 };
|
||||
Loading…
Add table
Reference in a new issue