├── .env.sample ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── docker-compose.yml ├── drizzle.config.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── icon.png ├── next.svg ├── no-data.svg └── vercel.svg ├── src ├── app │ ├── actions.ts │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── browse │ │ ├── page.tsx │ │ ├── room-card.tsx │ │ └── search-bar.tsx │ ├── create-room │ │ ├── actions.ts │ │ ├── create-room-form.tsx │ │ └── page.tsx │ ├── edit-room │ │ └── [roomId] │ │ │ ├── actions.ts │ │ │ ├── edit-room-form.tsx │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── header.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── provider.tsx │ ├── rooms │ │ └── [roomId] │ │ │ ├── actions.ts │ │ │ ├── page.tsx │ │ │ └── video-player.tsx │ └── your-rooms │ │ ├── actions.ts │ │ ├── page.tsx │ │ └── user-room-card.tsx ├── components │ ├── mode-toggle.tsx │ ├── tags-list.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── data-access │ ├── rooms.ts │ └── users.ts ├── db │ ├── index.ts │ └── schema.ts ├── lib │ ├── auth.ts │ └── utils.ts └── middleware.ts ├── tailwind.config.ts └── tsconfig.json /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL="REPLACE ME WITH THE REAL URL OF YOUR POSTGRES DATABASE" 2 | GOOGLE_CLIENT_ID="" 3 | GOOGLE_CLIENT_SECRET="" 4 | NEXTAUTH_SECRET="make_this_something_secret" 5 | NEXT_PUBLIC_GET_STREAM_API_KEY="" 6 | GET_STREAM_SECRET_KEY="" 7 | NEXTAUTH_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | 39 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Web Dev Cody 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | dev-finder-db: 4 | image: postgres 5 | restart: always 6 | container_name: dev-finder-db 7 | ports: 8 | - 5432:5432 9 | environment: 10 | POSTGRES_PASSWORD: example 11 | PGDATA: /data/postgres 12 | volumes: 13 | - postgres:/data/postgres 14 | 15 | volumes: 16 | postgres: 17 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | export default defineConfig({ 3 | schema: "./src/db/schema.ts", 4 | driver: "pg", 5 | dbCredentials: { 6 | connectionString: process.env.DATABASE_URL!, 7 | }, 8 | verbose: true, 9 | strict: true, 10 | }); 11 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-finder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:push": "drizzle-kit push:pg --config=drizzle.config.ts", 11 | "db:studio": "drizzle-kit studio" 12 | }, 13 | "dependencies": { 14 | "@auth/drizzle-adapter": "^0.8.0", 15 | "@hookform/resolvers": "^3.3.4", 16 | "@radix-ui/react-alert-dialog": "^1.0.5", 17 | "@radix-ui/react-avatar": "^1.0.4", 18 | "@radix-ui/react-dropdown-menu": "^2.0.6", 19 | "@radix-ui/react-label": "^2.0.2", 20 | "@radix-ui/react-slot": "^1.0.2", 21 | "@radix-ui/react-toast": "^1.1.5", 22 | "@stream-io/video-react-sdk": "^0.5.1", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.0", 25 | "drizzle-orm": "^0.30.0", 26 | "lucide-react": "^0.350.0", 27 | "next": "14.1.3", 28 | "next-auth": "^4.24.7", 29 | "next-themes": "^0.2.1", 30 | "nextjs-toploader": "^1.6.6", 31 | "pg": "^8.11.3", 32 | "postgres": "^3.4.3", 33 | "react": "^18", 34 | "react-dom": "^18", 35 | "react-hook-form": "^7.51.0", 36 | "stream-chat": "^8.22.0", 37 | "stream-chat-react": "^11.12.0", 38 | "tailwind-merge": "^2.2.1", 39 | "tailwindcss-animate": "^1.0.7", 40 | "zod": "^3.22.4" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20", 44 | "@types/react": "^18", 45 | "@types/react-dom": "^18", 46 | "autoprefixer": "^10.0.1", 47 | "drizzle-kit": "^0.20.14", 48 | "eslint": "^8", 49 | "eslint-config-next": "14.1.3", 50 | "postcss": "^8", 51 | "tailwindcss": "^3.3.0", 52 | "typescript": "^5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/dev-finder/c0638d62eb727f8a30a9ccbd84694565bc2ccc4b/public/icon.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/no-data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { deleteUser } from "@/data-access/users"; 4 | import { getSession } from "@/lib/auth"; 5 | 6 | export async function deleteAccountAction() { 7 | const session = await getSession(); 8 | 9 | if (!session) { 10 | throw new Error("you must be logged in to delete your account"); 11 | } 12 | 13 | await deleteUser(session.user.id); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import { authConfig } from "@/lib/auth"; 3 | 4 | const handler = NextAuth(authConfig); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /src/app/browse/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import Link from "next/link"; 3 | import { getRooms } from "@/data-access/rooms"; 4 | import { SearchBar } from "./search-bar"; 5 | import { RoomCard } from "./room-card"; 6 | import { unstable_noStore } from "next/cache"; 7 | import Image from "next/image"; 8 | 9 | export default async function Home({ 10 | searchParams, 11 | }: { 12 | searchParams: { 13 | search: string; 14 | }; 15 | }) { 16 | unstable_noStore(); 17 | const rooms = await getRooms(searchParams.search); 18 | 19 | return ( 20 |
21 |
22 |

Find Dev Rooms

23 | 26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 | {rooms.map((room) => { 34 | return ; 35 | })} 36 |
37 | 38 | {rooms.length === 0 && ( 39 |
40 | no data image 46 | 47 |

No Rooms Yet!

48 | 49 | 52 |
53 | )} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/browse/room-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Link from "next/link"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardFooter, 10 | CardHeader, 11 | CardTitle, 12 | } from "@/components/ui/card"; 13 | import { Room } from "@/db/schema"; 14 | import { GithubIcon } from "lucide-react"; 15 | import { TagsList } from "@/components/tags-list"; 16 | import { splitTags } from "@/lib/utils"; 17 | 18 | export function RoomCard({ room }: { room: Room }) { 19 | return ( 20 | 21 | 22 | {room.name} 23 | {room.description} 24 | 25 | 26 | 27 | {room.githubRepo && ( 28 | 34 | 35 | Github Project 36 | 37 | )} 38 | 39 | 40 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/browse/search-bar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { z } from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormMessage, 13 | } from "@/components/ui/form"; 14 | import { Input } from "@/components/ui/input"; 15 | import { SearchIcon } from "lucide-react"; 16 | import { useRouter, useSearchParams } from "next/navigation"; 17 | import { useEffect } from "react"; 18 | 19 | const formSchema = z.object({ 20 | search: z.string().min(0).max(50), 21 | }); 22 | 23 | export function SearchBar() { 24 | const router = useRouter(); 25 | const query = useSearchParams(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(formSchema), 29 | defaultValues: { 30 | search: query.get("search") ?? "", 31 | }, 32 | }); 33 | 34 | const search = query.get("search"); 35 | 36 | useEffect(() => { 37 | form.setValue("search", search ?? ""); 38 | }, [search, form]); 39 | 40 | async function onSubmit(values: z.infer) { 41 | if (values.search) { 42 | router.push(`/browse?search=${values.search}`); 43 | } else { 44 | router.push("/browse"); 45 | } 46 | } 47 | 48 | return ( 49 |
50 | 51 | ( 55 | 56 | 57 | 62 | 63 | 64 | 65 | )} 66 | /> 67 | 68 | 71 | 72 | {query.get("search") && ( 73 | 82 | )} 83 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/app/create-room/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { createRoom } from "@/data-access/rooms"; 4 | import { Room } from "@/db/schema"; 5 | import { getSession } from "@/lib/auth"; 6 | import { revalidatePath } from "next/cache"; 7 | 8 | export async function createRoomAction(roomData: Omit) { 9 | const session = await getSession(); 10 | 11 | if (!session) { 12 | throw new Error("you must be logged in to create this room"); 13 | } 14 | 15 | const room = await createRoom(roomData, session.user.id); 16 | 17 | revalidatePath("/browse"); 18 | 19 | return room; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/create-room/create-room-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { z } from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormDescription, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage, 15 | } from "@/components/ui/form"; 16 | import { Input } from "@/components/ui/input"; 17 | import { createRoomAction } from "./actions"; 18 | import { useRouter } from "next/navigation"; 19 | import { useToast } from "@/components/ui/use-toast"; 20 | 21 | const formSchema = z.object({ 22 | name: z.string().min(1).max(50), 23 | description: z.string().min(1).max(250), 24 | githubRepo: z.string().min(1).max(50), 25 | tags: z.string().min(1).max(50), 26 | }); 27 | 28 | export function CreateRoomForm() { 29 | const { toast } = useToast(); 30 | 31 | const router = useRouter(); 32 | 33 | const form = useForm>({ 34 | resolver: zodResolver(formSchema), 35 | defaultValues: { 36 | name: "", 37 | description: "", 38 | githubRepo: "", 39 | tags: "", 40 | }, 41 | }); 42 | 43 | async function onSubmit(values: z.infer) { 44 | const room = await createRoomAction(values); 45 | toast({ 46 | title: "Room Created", 47 | description: "Your room was successfully created", 48 | }); 49 | router.push(`/rooms/${room.id}`); 50 | } 51 | 52 | return ( 53 |
54 | 55 | ( 59 | 60 | Name 61 | 62 | 63 | 64 | This is your public room name. 65 | 66 | 67 | )} 68 | /> 69 | 70 | ( 74 | 75 | Description 76 | 77 | 81 | 82 | 83 | Please describe what you are be coding on 84 | 85 | 86 | 87 | )} 88 | /> 89 | 90 | ( 94 | 95 | Github Repo 96 | 97 | 101 | 102 | 103 | Please put a link to the project you are working on 104 | 105 | 106 | 107 | )} 108 | /> 109 | 110 | ( 114 | 115 | Tags 116 | 117 | 118 | 119 | 120 | List your programming languages, frameworks, libraries so people 121 | can find you content 122 | 123 | 124 | 125 | )} 126 | /> 127 | 128 | 129 | 130 | 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/app/create-room/page.tsx: -------------------------------------------------------------------------------- 1 | import { CreateRoomForm } from "./create-room-form"; 2 | 3 | export default function CreateRoomPage() { 4 | return ( 5 |
6 |

Create Room

7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/edit-room/[roomId]/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { editRoom, getRoom } from "@/data-access/rooms"; 4 | import { Room } from "@/db/schema"; 5 | import { getSession } from "@/lib/auth"; 6 | import { revalidatePath } from "next/cache"; 7 | import { redirect } from "next/navigation"; 8 | 9 | export async function editRoomAction(roomData: Omit) { 10 | const session = await getSession(); 11 | 12 | if (!session) { 13 | throw new Error("you must be logged in to create this room"); 14 | } 15 | 16 | const room = await getRoom(roomData.id); 17 | 18 | if (room?.userId !== session.user.id) { 19 | throw new Error("User not authorized"); 20 | } 21 | 22 | await editRoom({ ...roomData, userId: room.userId }); 23 | 24 | revalidatePath("/your-rooms"); 25 | revalidatePath(`/edit-room/${roomData.id}`); 26 | redirect("/your-rooms"); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/edit-room/[roomId]/edit-room-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { z } from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormDescription, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage, 15 | } from "@/components/ui/form"; 16 | import { Input } from "@/components/ui/input"; 17 | import { editRoomAction } from "./actions"; 18 | import { useParams } from "next/navigation"; 19 | import { Room } from "@/db/schema"; 20 | import { toast } from "@/components/ui/use-toast"; 21 | 22 | const formSchema = z.object({ 23 | name: z.string().min(1).max(50), 24 | description: z.string().min(1).max(250), 25 | githubRepo: z.string().min(1).max(50), 26 | tags: z.string().min(1).max(50), 27 | }); 28 | 29 | export function EditRoomForm({ room }: { room: Room }) { 30 | const params = useParams(); 31 | const form = useForm>({ 32 | resolver: zodResolver(formSchema), 33 | defaultValues: { 34 | name: room.name, 35 | description: room.description ?? "", 36 | githubRepo: room.githubRepo ?? "", 37 | tags: room.tags, 38 | }, 39 | }); 40 | 41 | async function onSubmit(values: z.infer) { 42 | await editRoomAction({ 43 | id: params.roomId as string, 44 | ...values, 45 | }); 46 | toast({ 47 | title: "Room Updated", 48 | description: "Your room was successfully updated", 49 | }); 50 | } 51 | 52 | return ( 53 |
54 | 55 | ( 59 | 60 | Name 61 | 62 | 63 | 64 | This is your public room name. 65 | 66 | 67 | )} 68 | /> 69 | 70 | ( 74 | 75 | Description 76 | 77 | 81 | 82 | 83 | Please describe what you are be coding on 84 | 85 | 86 | 87 | )} 88 | /> 89 | 90 | ( 94 | 95 | Github Repo 96 | 97 | 101 | 102 | 103 | Please put a link to the project you are working on 104 | 105 | 106 | 107 | )} 108 | /> 109 | 110 | ( 114 | 115 | Tags 116 | 117 | 118 | 119 | 120 | List your programming languages, frameworks, libraries so people 121 | can find you content 122 | 123 | 124 | 125 | )} 126 | /> 127 | 128 | 129 | 130 | 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/app/edit-room/[roomId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getRoom } from "@/data-access/rooms"; 2 | import { EditRoomForm } from "./edit-room-form"; 3 | import { unstable_noStore } from "next/cache"; 4 | 5 | export default async function EditRoomPage({ 6 | params, 7 | }: { 8 | params: { roomId: string }; 9 | }) { 10 | unstable_noStore(); 11 | const room = await getRoom(params.roomId); 12 | 13 | if (!room) { 14 | return
Room not found
; 15 | } 16 | 17 | return ( 18 |
19 |

Edit Room

20 | 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/dev-finder/c0638d62eb727f8a30a9ccbd84694565bc2ccc4b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ModeToggle } from "@/components/mode-toggle"; 4 | import { Button } from "@/components/ui/button"; 5 | import { signIn, signOut, useSession } from "next-auth/react"; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | import { DeleteIcon, LogInIcon, LogOutIcon } from "lucide-react"; 13 | import Image from "next/image"; 14 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 15 | import Link from "next/link"; 16 | import { 17 | AlertDialog, 18 | AlertDialogAction, 19 | AlertDialogCancel, 20 | AlertDialogContent, 21 | AlertDialogDescription, 22 | AlertDialogFooter, 23 | AlertDialogHeader, 24 | AlertDialogTitle, 25 | AlertDialogTrigger, 26 | } from "@/components/ui/alert-dialog"; 27 | import { useState } from "react"; 28 | import { deleteAccountAction } from "./actions"; 29 | 30 | function AccountDropdown() { 31 | const session = useSession(); 32 | const [open, setOpen] = useState(false); 33 | 34 | return ( 35 | <> 36 | 37 | 38 | 39 | Are you absolutely sure? 40 | 41 | This action cannot be undone. This will permanently remove your 42 | account and any data your have. 43 | 44 | 45 | 46 | Cancel 47 | { 49 | await deleteAccountAction(); 50 | signOut({ callbackUrl: "/" }); 51 | }} 52 | > 53 | Yes, delete my account 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 73 | signOut({ 74 | callbackUrl: "/", 75 | }) 76 | } 77 | > 78 | Sign Out 79 | 80 | 81 | { 83 | setOpen(true); 84 | }} 85 | > 86 | Delete Account 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | 94 | export function Header() { 95 | const session = useSession(); 96 | const isLoggedIn = !!session.data; 97 | 98 | return ( 99 |
100 |
101 | 105 | the application icon of a magnifying glass 111 | DevFinder 112 | 113 | 114 | 127 | 128 |
129 | {isLoggedIn && } 130 | {!isLoggedIn && ( 131 | 134 | )} 135 | 136 |
137 |
138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Providers } from "./provider"; 5 | import { Header } from "./header"; 6 | import NextTopLoader from "nextjs-toploader"; 7 | import { Toaster } from "@/components/ui/toaster"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Dev Finder", 13 | description: 14 | "An application to help pair programming with random devs online", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode; 21 | }>) { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 |
29 |
{children}
30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | export default function LandingPage() { 5 | return ( 6 |
7 |
8 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/app/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider } from "@/components/theme-provider"; 4 | import { SessionProvider } from "next-auth/react"; 5 | import { ReactNode } from "react"; 6 | 7 | export function Providers({ children }: { children: ReactNode }) { 8 | return ( 9 | 10 | 16 | {children} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/rooms/[roomId]/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getSession } from "@/lib/auth"; 4 | import { StreamChat } from "stream-chat"; 5 | 6 | export async function generateTokenAction() { 7 | const session = await getSession(); 8 | 9 | if (!session) { 10 | throw new Error("No session found"); 11 | } 12 | 13 | const api_key = process.env.NEXT_PUBLIC_GET_STREAM_API_KEY!; 14 | const api_secret = process.env.GET_STREAM_SECRET_KEY!; 15 | const serverClient = StreamChat.getInstance(api_key, api_secret); 16 | const token = serverClient.createToken(session.user.id); 17 | console.log("token", token); 18 | return token; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/rooms/[roomId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { TagsList } from "@/components/tags-list"; 2 | import { getRoom } from "@/data-access/rooms"; 3 | import { GithubIcon } from "lucide-react"; 4 | import Link from "next/link"; 5 | import { DevFinderVideo } from "./video-player"; 6 | import { splitTags } from "@/lib/utils"; 7 | import { unstable_noStore } from "next/cache"; 8 | 9 | export default async function RoomPage(props: { params: { roomId: string } }) { 10 | unstable_noStore(); 11 | const roomId = props.params.roomId; 12 | 13 | const room = await getRoom(roomId); 14 | 15 | if (!room) { 16 | return
No room of this ID found
; 17 | } 18 | 19 | return ( 20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 |

{room?.name}

30 | 31 | {room.githubRepo && ( 32 | 38 | 39 | Github Project 40 | 41 | )} 42 | 43 |

{room?.description}

44 | 45 | 46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/app/rooms/[roomId]/video-player.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "@stream-io/video-react-sdk/dist/css/styles.css"; 4 | import { Room } from "@/db/schema"; 5 | import { 6 | Call, 7 | CallControls, 8 | CallParticipantsList, 9 | SpeakerLayout, 10 | StreamCall, 11 | StreamTheme, 12 | StreamVideo, 13 | StreamVideoClient, 14 | } from "@stream-io/video-react-sdk"; 15 | import { useSession } from "next-auth/react"; 16 | import { useCallback, useEffect, useState } from "react"; 17 | import { generateTokenAction } from "./actions"; 18 | import { useRouter } from "next/navigation"; 19 | 20 | const apiKey = process.env.NEXT_PUBLIC_GET_STREAM_API_KEY!; 21 | 22 | export function DevFinderVideo({ room }: { room: Room }) { 23 | const session = useSession(); 24 | const [client, setClient] = useState(null); 25 | const [call, setCall] = useState(null); 26 | const router = useRouter(); 27 | 28 | useEffect(() => { 29 | if (!room) return; 30 | if (!session.data) { 31 | return; 32 | } 33 | const userId = session.data.user.id; 34 | const client = new StreamVideoClient({ 35 | apiKey, 36 | user: { 37 | id: userId, 38 | name: session.data.user.name ?? undefined, 39 | image: session.data.user.image ?? undefined, 40 | }, 41 | tokenProvider: () => generateTokenAction(), 42 | }); 43 | const call = client.call("default", room.id); 44 | call.join({ create: true }); 45 | setClient(client); 46 | setCall(call); 47 | 48 | return () => { 49 | call 50 | .leave() 51 | .then(() => client.disconnectUser()) 52 | .catch(console.error); 53 | }; 54 | }, [session, room]); 55 | 56 | return ( 57 | client && 58 | call && ( 59 | 60 | 61 | 62 | 63 | { 65 | router.push("/"); 66 | }} 67 | /> 68 | undefined} /> 69 | 70 | 71 | 72 | ) 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/app/your-rooms/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { deleteRoom, getRoom } from "@/data-access/rooms"; 4 | import { getSession } from "@/lib/auth"; 5 | import { revalidatePath } from "next/cache"; 6 | 7 | export async function deleteRoomAction(roomId: string) { 8 | const session = await getSession(); 9 | if (!session) { 10 | throw new Error("User not authenticated"); 11 | } 12 | 13 | const room = await getRoom(roomId); 14 | 15 | if (room?.userId !== session.user.id) { 16 | throw new Error("User not authorized"); 17 | } 18 | 19 | await deleteRoom(roomId); 20 | 21 | revalidatePath("/your-rooms"); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/your-rooms/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import Link from "next/link"; 3 | import { getUserRooms } from "@/data-access/rooms"; 4 | import { UserRoomCard } from "./user-room-card"; 5 | import { unstable_noStore } from "next/cache"; 6 | import Image from "next/image"; 7 | 8 | export default async function YourRoomsPage() { 9 | unstable_noStore(); 10 | const rooms = await getUserRooms(); 11 | 12 | return ( 13 |
14 |
15 |

Your Rooms

16 | 19 |
20 | 21 |
22 | {rooms.map((room) => { 23 | return ; 24 | })} 25 |
26 | 27 | {rooms.length === 0 && ( 28 |
29 | no data image 35 | 36 |

You have no rooms

37 | 38 | 41 |
42 | )} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/your-rooms/user-room-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import Link from "next/link"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import { Room } from "@/db/schema"; 13 | import { GithubIcon, PencilIcon, TrashIcon } from "lucide-react"; 14 | import { TagsList } from "@/components/tags-list"; 15 | import { splitTags } from "@/lib/utils"; 16 | import { 17 | AlertDialog, 18 | AlertDialogAction, 19 | AlertDialogCancel, 20 | AlertDialogContent, 21 | AlertDialogDescription, 22 | AlertDialogFooter, 23 | AlertDialogHeader, 24 | AlertDialogTitle, 25 | AlertDialogTrigger, 26 | } from "@/components/ui/alert-dialog"; 27 | import { deleteRoomAction } from "./actions"; 28 | 29 | export function UserRoomCard({ room }: { room: Room }) { 30 | return ( 31 | 32 | 33 | 38 | {room.name} 39 | {room.description} 40 | 41 | 42 | 43 | {room.githubRepo && ( 44 | 50 | 51 | Github Project 52 | 53 | )} 54 | 55 | 56 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | Are you absolutely sure? 69 | 70 | This action cannot be undone. This will permanently remove the 71 | room and any data associated with it. 72 | 73 | 74 | 75 | Cancel 76 | { 78 | deleteRoomAction(room.id); 79 | }} 80 | > 81 | Yes, delete 82 | 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Moon, Sun } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/tags-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { badgeVariants } from "./ui/badge"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | export function TagsList({ tags }: { tags: string[] }) { 8 | const router = useRouter(); 9 | return ( 10 |
11 | {tags.map((tag) => ( 12 | 21 | ))} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |