diff --git a/.gitignore b/.gitignore index fd3dbb5..00bba9b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel diff --git a/bun.lockb b/bun.lockb index c5f367d..8b8efbf 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..14331d8 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/package.json b/package.json index bdd0586..0548c7a 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,32 @@ "lint": "next lint" }, "dependencies": { + "@clerk/nextjs": "^4.29.11", + "@prisma/client": "^5.12.1", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "next": "14.1.4", "react": "^18", "react-dom": "^18", - "next": "14.1.4" + "tailwind-merge": "^2.2.2", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", - "postcss": "^8", - "tailwindcss": "^3.3.0", "eslint": "^8", - "eslint-config-next": "14.1.4" + "eslint-config-next": "14.1.4", + "postcss": "^8", + "prisma": "^5.12.1", + "tailwindcss": "^3.3.0", + "typescript": "^5" } } diff --git a/prisma/migrations/20240405225206_init/migration.sql b/prisma/migrations/20240405225206_init/migration.sql new file mode 100644 index 0000000..850309f --- /dev/null +++ b/prisma/migrations/20240405225206_init/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "Point" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "number" INTEGER NOT NULL, + + CONSTRAINT "Point_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PointCount" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "balance" INTEGER NOT NULL, + + CONSTRAINT "PointCount_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20240405225349_reason/migration.sql b/prisma/migrations/20240405225349_reason/migration.sql new file mode 100644 index 0000000..1d59dbc --- /dev/null +++ b/prisma/migrations/20240405225349_reason/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `reason` to the `Point` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Point" ADD COLUMN "reason" TEXT NOT NULL; diff --git a/prisma/migrations/20240405225645_changeid/migration.sql b/prisma/migrations/20240405225645_changeid/migration.sql new file mode 100644 index 0000000..8f917f6 --- /dev/null +++ b/prisma/migrations/20240405225645_changeid/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - The primary key for the `PointCount` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `PointCount` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "PointCount" DROP CONSTRAINT "PointCount_pkey", +DROP COLUMN "id", +ADD CONSTRAINT "PointCount_pkey" PRIMARY KEY ("userId"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..a19de92 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,27 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Point { + id Int @id @default(autoincrement()) + userId String + createdAt DateTime @default(now()) + number Int + reason String +} + +model PointCount { + userId String @id + balance Int +} \ No newline at end of file diff --git a/src/app/fonts/Satoshi-Medium.woff2 b/src/app/fonts/Satoshi-Medium.woff2 new file mode 100644 index 0000000..ffd0ac9 Binary files /dev/null and b/src/app/fonts/Satoshi-Medium.woff2 differ diff --git a/src/app/globals.css b/src/app/globals.css index 875c01e..9990664 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,32 +2,57 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { +@layer base { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --primary: 220.47 98.26% 36.08%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 92.99% 56.11%; + --destructive-foreground: 210 40% 98%; + --ring: 220.67 97.83% 36.08%; + --radius: 0.5rem; + } + + .dark { + --background: 240 13.73% 10%; + --foreground: 229.76 31.78% 74.71%; + --muted: 232.5 15.44% 18.32%; + --muted-foreground: 233.79 11.37% 50%; + --popover: 234.55 17.46% 12.35%; + --popover-foreground: 234 12.4% 52.55%; + --card: 234.55 17.46% 12.35%; + --card-foreground: 229.76 31.78% 74.71%; + --border: 232.5 15.38% 30.59%; + --input: 232 20% 14.71%; + --primary: 0 0% 82.75%; + --primary-foreground: 0 0% 20%; + --secondary: 225.45 71.22% 72.75%; + --secondary-foreground: 234.55 17.46% 12.35%; + --accent: 234.55 17.83% 9.47%; + --accent-foreground: 0 0% 82.75%; + --destructive: 1.58 47.5% 52.94%; + --destructive-foreground: 210 40% 98.04%; + --ring: 225.45 71.22% 72.75%; } } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} +input[type='number']::-webkit-outer-spin-button, + input[type='number']::-webkit-inner-spin-button, + input[type='number'] { + -webkit-appearance: none; + margin: 0; + -moz-appearance: textfield !important; + } \ No newline at end of file diff --git a/src/app/history/page.tsx b/src/app/history/page.tsx new file mode 100644 index 0000000..2ee6523 --- /dev/null +++ b/src/app/history/page.tsx @@ -0,0 +1,19 @@ +import History from '@/components/app/History/History' +import prisma from '@/lib/db' +import { currentUser } from '@clerk/nextjs' + +export default async function Page() { + const pointHistory = (await prisma.point.findMany({ + where: { + userId: (await currentUser())!.id + } + })).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).map(p => { + return { + date: p.createdAt, + reason: p.reason, + points: p.number, + id: p.id + } + }) + return pointHistory.map(p => ) +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3314e47..e513cf7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,19 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import localFont from 'next/font/local' import "./globals.css"; +import { ClerkProvider } from "@clerk/nextjs"; +import Navbar from "@/components/app/Navbar/Navbar"; -const inter = Inter({ subsets: ["latin"] }); +const satoshi = localFont({ src: './fonts/Satoshi-Medium.woff2' }) export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Puntos app", + description: "haha yes points go brrr", + openGraph: { + type: 'website', + locale: 'es_ES', + url: 'https://puntos.srizan.dev', + } }; export default function RootLayout({ @@ -15,8 +22,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - {children} - + + + + +
+ {children} +
+ + +
); } diff --git a/src/app/ok/page.tsx b/src/app/ok/page.tsx new file mode 100644 index 0000000..e114371 --- /dev/null +++ b/src/app/ok/page.tsx @@ -0,0 +1,13 @@ +import { Button } from "@/components/ui/button"; +import Link from "next/link"; + +export default function Page() { + return ( +
+

nice

+ + + +
+ ) +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index b81507d..dfd6231 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,113 +1,19 @@ -import Image from "next/image"; +import DesktopPoints from "@/components/app/Points/Desktop/Desktop"; +import prisma from "@/lib/db"; +import { currentUser } from "@clerk/nextjs"; -export default function Home() { +export default async function Home() { + const pointCount = (await prisma.pointCount.findFirst({ + where: { + userId: (await currentUser())!.id, + } + }))!.balance return ( -
-
-

- Get started by editing  - src/app/page.tsx -

-
- - By{" "} - Vercel Logo - -
+ <> +

tienes {pointCount} puntos

+
+
- -
- Next.js Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
-
+ ); } diff --git a/src/app/remove/page.tsx b/src/app/remove/page.tsx new file mode 100644 index 0000000..3d5de9c --- /dev/null +++ b/src/app/remove/page.tsx @@ -0,0 +1,19 @@ +import RemovePoints from "@/components/app/RemovePoints/RemovePoints" +import prisma from "@/lib/db" +import { currentUser } from "@clerk/nextjs" + +export default async function Page() { + const pointCount = (await prisma.pointCount.findFirst({ + where: { + userId: (await currentUser())!.id, + } + }))!.balance + return ( + <> +

tienes {pointCount} puntos

+
+ +
+ + ) +} \ No newline at end of file diff --git a/src/components/app/History/History.tsx b/src/components/app/History/History.tsx new file mode 100644 index 0000000..5df43e7 --- /dev/null +++ b/src/components/app/History/History.tsx @@ -0,0 +1,105 @@ +/** + * v0 by Vercel. + * @see https://v0.dev/t/EhXenFqofUT + * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app + */ +import { CardContent, Card } from "@/components/ui/card" +import React, { PropsWithoutRef } from "react" + +export default function History(props: Props) { + const pointsColor = props.points > 0 ? "text-green-500" : "text-red-500" + return ( + + +
+
+
+ + {props.date.toLocaleDateString()} +
+
+ + {props.reason} +
+
+ {props.points} puntos +
+
+
+
+
+ ) +} + +interface Props { + date: Date + reason: string + points: number +} + +function CalendarIcon(props: React.SVGProps) { + return ( + + + + + + + ) +} + + +function CoffeeIcon(props: React.SVGProps) { + return ( + + + + + + + + ) +} + + +function CreditCardIcon(props: React.SVGProps) { + return ( + + + + + ) +} diff --git a/src/components/app/Navbar/Navbar.tsx b/src/components/app/Navbar/Navbar.tsx new file mode 100644 index 0000000..2443699 --- /dev/null +++ b/src/components/app/Navbar/Navbar.tsx @@ -0,0 +1,51 @@ +/** + * v0 by Vercel. + * @see https://v0.dev/t/igzEEdGqAvH + * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app + */ +import { Button } from "@/components/ui/button" +import { DropdownMenuShortcut } from "@/components/ui/dropdown-menu" +import { UserButton } from "@clerk/nextjs" +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuItem } from "@/components/ui/dropdown-menu" +import Link from "next/link" + +export default function Navbar() { + return ( + + ) +} + diff --git a/src/components/app/Points/Desktop/Desktop.tsx b/src/components/app/Points/Desktop/Desktop.tsx new file mode 100644 index 0000000..e48f580 --- /dev/null +++ b/src/components/app/Points/Desktop/Desktop.tsx @@ -0,0 +1,69 @@ +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import prisma from "@/lib/db" +import { redirect } from "next/navigation" +import { currentUser } from "@clerk/nextjs" + +export default function DesktopPoints() { + async function createPoints(formData: FormData) { + 'use server' + + const rawFormData = { + points: formData.get('points'), + reason: formData.get('reason'), + } + + await prisma.point.create({ + data: { + userId: (await currentUser())!.id, + number: Number(rawFormData.points), + reason: rawFormData.reason as string, + } + }) + await prisma.pointCount.upsert({ + where: { + userId: (await currentUser())!.id, + }, + update: { + balance: { + increment: Number(rawFormData.points), + } + }, + create: { + userId: (await currentUser())!.id, + balance: Number(rawFormData.points), + } + }) + redirect('/ok') + } + return ( + + + Añade o elimina puntos + wow enhorabuena + +
+ +
+
+ + +
+
+
+ + + +
+
+ ) +} diff --git a/src/components/app/RemovePoints/RemovePoints.tsx b/src/components/app/RemovePoints/RemovePoints.tsx new file mode 100644 index 0000000..2b89961 --- /dev/null +++ b/src/components/app/RemovePoints/RemovePoints.tsx @@ -0,0 +1,69 @@ +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import prisma from "@/lib/db" +import { redirect } from "next/navigation" +import { currentUser } from "@clerk/nextjs" + +export default function RemovePoints() { + async function createPoints(formData: FormData) { + 'use server' + + const rawFormData = { + points: `-${formData.get('points')}`, + reason: formData.get('reason'), + } + + await prisma.point.create({ + data: { + userId: (await currentUser())!.id, + number: Number(rawFormData.points), + reason: rawFormData.reason as string, + } + }) + await prisma.pointCount.upsert({ + where: { + userId: (await currentUser())!.id, + }, + update: { + balance: { + increment: Number(rawFormData.points), + } + }, + create: { + userId: (await currentUser())!.id, + balance: Number(rawFormData.points), + } + }) + redirect('/ok') + } + return ( + + + Elimina puntos + oh no + +
+ +
+
+ + +
+
+
+ + + +
+
+ ) +} \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0270f64 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..7d2b3c3 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..242b07a --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts new file mode 100644 index 0000000..f4ba39b --- /dev/null +++ b/src/lib/db/index.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from '@prisma/client' + +const prismaClientSingleton = () => { + return new PrismaClient() +} + +declare global { + var prismaGlobal: undefined | ReturnType +} + +const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() + +export default prisma + +if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..3e006d2 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,7 @@ +import { authMiddleware } from "@clerk/nextjs"; + +export default authMiddleware({}); + +export const config = { + matcher: ["/((?!.+.[w]+$|_next).*)", "/", "/(api|trpc)(.*)"], +}; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index e9a0944..84287e8 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,20 +1,80 @@ -import type { Config } from "tailwindcss"; +import type { Config } from "tailwindcss" -const config: Config = { +const config = { + darkMode: ["class"], content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", }, }, }, - plugins: [], -}; -export default config; + plugins: [require("tailwindcss-animate")], +} satisfies Config + +export default config \ No newline at end of file