this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(home): working

+103 -162
+63 -106
src/app/page.tsx
··· 1 + "use client"; 2 + 3 + import Link from "next/link"; 4 + import { useEffect, useState } from "react"; 5 + import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 6 + import { Button } from "@/components/ui/button"; 1 7 import { 2 8 Card, 3 9 CardAction, ··· 6 12 CardHeader, 7 13 CardTitle, 8 14 } from "@/components/ui/card"; 9 - 10 - import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 11 - import { Button } from "@/components/ui/button"; 12 - import { 13 - Empty, 14 - EmptyHeader, 15 - EmptyMedia, 16 - EmptyTitle, 17 - } from "@/components/ui/empty"; 18 - import { PlusSignIcon } from "@hugeicons/core-free-icons"; 19 - import { HugeiconsIcon } from "@hugeicons/react"; 20 - import Link from "next/link"; 21 - import { 22 - Dialog, 23 - DialogContent, 24 - DialogHeader, 25 - DialogTitle, 26 - DialogTrigger, 27 - } from "@/components/ui/dialog"; 15 + import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; 28 16 import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 29 17 import { Input } from "@/components/ui/input"; 30 - import { auth } from "@/lib/auth"; 31 - import { headers } from "next/headers"; 32 - import { redirect } from "next/navigation"; 18 + import { authClient } from "@/lib/auth-client"; 19 + import { createGroup, getGroups } from "@/lib/group"; 20 + import { getInitials } from "@/lib/utils"; 21 + 22 + type Group = { 23 + id: number; 24 + userId: string; 25 + name: string; 26 + }; 33 27 34 - export default async function Groups() { 35 - const session = await auth.api.getSession({ 36 - headers: await headers() 37 - }) 28 + export default function Home() { 29 + const { data: session } = authClient.useSession(); 30 + const user = session?.user; 31 + 32 + const [groups, setGroups] = useState<Group[]>([]); 33 + const [open, setOpen] = useState(false); 38 34 39 - if (!session) 40 - redirect("/auth") 35 + useEffect(() => { 36 + const id = user?.id; 37 + if (!id) return; 38 + getGroups(id).then(setGroups); 39 + }, [user]); 41 40 42 - const groups = [ 43 - { 44 - id: 1, 45 - emoji: "❤️", 46 - title: "Love & Stuff", 47 - stats: { 48 - total: 10, 49 - members: 5, 50 - }, 51 - host: true, 52 - }, 53 - { 54 - id: 2, 55 - emoji: "👩‍💻", 56 - title: "Work Buddies", 57 - stats: { 58 - total: 20, 59 - members: 10, 60 - }, 61 - host: false, 62 - }, 63 - ]; 41 + if (!user) return null; 64 42 65 43 return ( 66 - <Card className="ring-0"> 44 + <Card> 67 45 <CardHeader> 68 46 <CardTitle>Vouch</CardTitle> 69 47 <CardDescription>Your shared promises</CardDescription> 70 48 <CardAction> 71 - <Link href="/settings"> 72 - <Avatar> 73 - <AvatarImage src="https://github.com/shadcn.png" /> 74 - <AvatarFallback>CN</AvatarFallback> 75 - </Avatar> 76 - </Link> 49 + <Avatar render={<Link href="/settings" />}> 50 + <AvatarImage src={user.image as string} /> 51 + <AvatarFallback>{getInitials(user.name)}</AvatarFallback> 52 + </Avatar> 77 53 </CardAction> 78 54 </CardHeader> 79 - <CardContent className="grid gap-6"> 55 + <CardContent className="grid gap-4"> 80 56 {groups.map((group) => ( 81 57 <Link key={group.id} href={`/${group.id}`}> 82 58 <Card> 83 59 <CardHeader> 84 - <Button variant="secondary" size="icon"> 85 - {group.emoji} 86 - </Button> 87 - <CardTitle>{group.title}</CardTitle> 88 - <CardDescription> 89 - {group.stats.total} vouches, {group.stats.members} members 90 - </CardDescription> 91 - {group.host && ( 92 - <CardAction> 93 - <Button>Host</Button> 94 - </CardAction> 95 - )} 60 + <CardTitle>{group.name}</CardTitle> 61 + {/* <CardDescription> 62 + {group.stats.members} members, {group.stats.total} vouches 63 + </CardDescription> */} 64 + {group.userId === user.id && <CardAction>Host</CardAction>} 96 65 </CardHeader> 97 66 </Card> 98 67 </Link> 99 68 ))} 100 - <Dialog> 101 - <DialogTrigger 102 - render={ 103 - <Empty className="border border-dashed"> 104 - <EmptyHeader> 105 - <EmptyMedia variant="icon"> 106 - <HugeiconsIcon icon={PlusSignIcon} /> 107 - </EmptyMedia> 108 - <EmptyTitle>New Group</EmptyTitle> 109 - </EmptyHeader> 110 - </Empty> 111 - } 112 - /> 69 + <Dialog open={open} onOpenChange={setOpen}> 70 + <DialogTrigger render={<Button>Create group</Button>} /> 113 71 <DialogContent> 114 - <DialogHeader> 115 - <DialogTitle>Create a new group</DialogTitle> 116 - </DialogHeader> 117 - <form> 72 + <form 73 + onSubmit={(e) => { 74 + e.preventDefault(); 75 + const formData = new FormData(e.currentTarget); 76 + const name = formData.get("name"); 77 + createGroup(name as string).then((res) => { 78 + if (!res?.length) return; 79 + setGroups((prevGroups) => [...prevGroups, ...res]); 80 + setOpen(false); 81 + }); 82 + }} 83 + > 118 84 <FieldGroup> 119 - <div className="flex gap-4"> 120 - <Field> 121 - <FieldLabel htmlFor="form-emoji">Emoji</FieldLabel> 122 - <Input 123 - id="form-emoji" 124 - placeholder="Enter the emoji" 125 - required 126 - /> 127 - </Field> 128 - <Field> 129 - <FieldLabel htmlFor="form-name">Name</FieldLabel> 130 - <Input 131 - id="form-name" 132 - placeholder="Enter the name" 133 - required 134 - /> 135 - </Field> 136 - </div> 137 - <Field className="flex flex-col"> 85 + <Field> 86 + <FieldLabel htmlFor="name">Name</FieldLabel> 87 + <Input 88 + id="name" 89 + name="name" 90 + placeholder="Something cool" 91 + required 92 + /> 93 + </Field> 94 + <Field> 138 95 <Button type="submit">Create</Button> 139 96 </Field> 140 97 </FieldGroup>
+3 -3
src/app/settings/page.tsx
··· 5 5 import Link from "next/link"; 6 6 import { Button } from "@/components/ui/button"; 7 7 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 8 - import { Field, FieldGroup, FieldLabel,} from "@/components/ui/field"; 8 + import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 9 9 import { Input } from "@/components/ui/input"; 10 10 import { authClient } from "@/lib/auth-client"; 11 11 import { uploadImage } from "@/lib/image"; ··· 38 38 39 39 if (email && email !== user.email) 40 40 await authClient.changeEmail({ 41 - newEmail: email as string, 42 - callbackURL: "/", 41 + newEmail: email as string, 42 + callbackURL: "/", 43 43 }); 44 44 }; 45 45
+2 -2
src/db/schema.ts
··· 1 1 import { integer, pgTable, text, varchar } from "drizzle-orm/pg-core"; 2 2 import { user } from "./auth-schema"; 3 3 4 - export const like = pgTable("like", { 4 + export const group = pgTable("group", { 5 5 id: integer().primaryKey().generatedAlwaysAsIdentity(), 6 6 userId: text() 7 7 .notNull() 8 8 .references(() => user.id), 9 - slug: varchar().notNull(), 9 + name: varchar().notNull(), 10 10 }); 11 11 12 12 export * from "./auth-schema";
+10 -4
src/lib/auth.ts
··· 1 1 import { betterAuth } from "better-auth"; 2 2 import { drizzleAdapter } from "better-auth/adapters/drizzle"; 3 3 import { magicLink } from "better-auth/plugins"; 4 - 4 + import { headers } from "next/headers"; 5 5 import * as schema from "@/db/schema"; 6 6 import { db } from "@/lib/db"; 7 7 import { sendEmail } from "@/lib/email"; 8 8 9 + //TODO: replace login with Google 9 10 export const auth = betterAuth({ 10 11 database: drizzleAdapter(db, { 11 12 provider: "pg", 12 13 schema, 13 14 }), 14 - user: { changeEmail: { enabled: true, } }, 15 + user: { changeEmail: { enabled: true } }, 15 16 emailVerification: { 16 17 sendVerificationEmail: async ({ user, url, token }) => { 17 18 await sendEmail({ ··· 23 24 <a href="${url}">Verify Email</a> 24 25 `, 25 26 }); 26 - } 27 + }, 27 28 }, 28 29 plugins: [ 29 30 magicLink({ ··· 39 40 }); 40 41 }, 41 42 }), 42 - ] 43 + ], 43 44 }); 45 + 46 + export async function getUserId() { 47 + const data = await auth.api.getSession({ headers: await headers() }); 48 + return data?.user.id ?? null; 49 + }
+19
src/lib/group.ts
··· 1 + "use server"; 2 + 3 + import { eq } from "drizzle-orm"; 4 + 5 + import { group } from "@/db/schema"; 6 + import { getUserId } from "@/lib/auth"; 7 + import { db } from "@/lib/db"; 8 + 9 + export async function getGroups(id: string) { 10 + return await db.query.group.findMany({ 11 + where: eq(group.userId, id), 12 + }); 13 + } 14 + 15 + export async function createGroup(name: string) { 16 + const userId = await getUserId(); 17 + if (!userId) return null; 18 + return await db.insert(group).values({ name, userId }).returning(); 19 + }
+1
src/lib/image.ts
··· 3 3 import { put } from "@vercel/blob"; 4 4 5 5 export async function uploadImage(file: File) { 6 + //TODO: remove the old file 6 7 return put(file.name, file, { 7 8 access: "public", 8 9 });
-46
src/lib/like.ts
··· 1 - "use server"; 2 - 3 - import { and, count, eq } from "drizzle-orm"; 4 - import { headers } from "next/headers"; 5 - 6 - import { like } from "@/db/schema"; 7 - import { auth } from "@/lib/auth"; 8 - import { db } from "@/lib/db"; 9 - 10 - async function getUserId() { 11 - const data = await auth.api.getSession({ headers: await headers() }); 12 - return data?.user.id ?? null; 13 - } 14 - 15 - export async function isLiked(slug: string) { 16 - const userId = await getUserId(); 17 - if (!userId) return false; 18 - 19 - const existing = await db.query.like.findFirst({ 20 - where: and(eq(like.userId, userId), eq(like.slug, slug)), 21 - columns: { id: true }, 22 - }); 23 - 24 - return !!existing; 25 - } 26 - 27 - export async function toggleLike(slug: string) { 28 - const userId = await getUserId(); 29 - if (!userId) return; 30 - 31 - const deleted = await db 32 - .delete(like) 33 - .where(and(eq(like.userId, userId), eq(like.slug, slug))) 34 - .returning({ id: like.id }); 35 - 36 - if (deleted.length === 0) 37 - await db.insert(like).values({ slug, userId }).onConflictDoNothing(); 38 - } 39 - 40 - export async function getlikeCount(slug: string) { 41 - const res = await db 42 - .select({ value: count() }) 43 - .from(like) 44 - .where(eq(like.slug, slug)); 45 - return res[0]?.value ?? 0; 46 - }
+5 -1
src/lib/utils.ts
··· 1 - import { clsx, type ClassValue } from "clsx"; 1 + import { type ClassValue, clsx } from "clsx"; 2 2 import { twMerge } from "tailwind-merge"; 3 3 4 4 export function cn(...inputs: ClassValue[]) { 5 5 return twMerge(clsx(inputs)); 6 6 } 7 + 8 + export function getInitials(name: string) { 9 + return name.match(/[A-Z]/g); 10 + }