diff --git a/.env b/.env new file mode 100644 index 0000000..498ab17 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://postgres:postgres@localhost:5432/{DB_NAME} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..773bb37 Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..33fb800 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..2175e39 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "drizzle-kit"; +import { env } from "@/lib/env.mjs"; + +export default { + schema: "./src/lib/db/schema", + out: "./src/lib/db/migrations", + driver: "pg", + dbCredentials: { + connectionString: env.DATABASE_URL, + } +} satisfies Config; \ No newline at end of file diff --git a/kirimase.config.json b/kirimase.config.json new file mode 100644 index 0000000..ca52dc2 --- /dev/null +++ b/kirimase.config.json @@ -0,0 +1,17 @@ +{ + "hasSrc": true, + "packages": [ + "shadcn-ui", + "drizzle" + ], + "preferredPackageManager": "bun", + "t3": false, + "alias": "@", + "analytics": true, + "rootPath": "src/", + "componentLib": "shadcn-ui", + "driver": "pg", + "provider": "postgresjs", + "orm": "drizzle", + "auth": null +} \ No newline at end of file diff --git a/package.json b/package.json index 233f80e..e9ee847 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,49 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "db:generate": "drizzle-kit generate:pg", + "db:migrate": "tsx src/lib/db/migrate.ts", + "db:drop": "drizzle-kit drop", + "db:pull": "drizzle-kit introspect:pg", + "db:studio": "drizzle-kit studio", + "db:check": "drizzle-kit check:pg" }, "dependencies": { + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-slot": "^1.0.2", + "@t3-oss/env-nextjs": "^0.9.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "drizzle-orm": "^0.30.2", + "drizzle-zod": "^0.5.1", + "lucide-react": "^0.358.0", + "nanoid": "^5.0.6", + "next": "14.1.3", + "next-themes": "^0.3.0", + "postgres": "^3.4.3", "react": "^18", "react-dom": "^18", - "next": "14.1.3" + "sonner": "^1.4.3", + "tailwind-merge": "^2.2.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "dotenv": "^16.4.5", + "drizzle-kit": "^0.20.14", + "eslint": "^8", + "eslint-config-next": "14.1.3", + "pg": "^8.11.3", "postcss": "^8", "tailwindcss": "^3.3.0", - "eslint": "^8", - "eslint-config-next": "14.1.3" + "tsx": "^4.7.1", + "typescript": "^5" } -} +} \ No newline at end of file diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx new file mode 100644 index 0000000..13149b0 --- /dev/null +++ b/src/app/(app)/dashboard/page.tsx @@ -0,0 +1,10 @@ +export default function Home() { + return ( +
+

Home

+

+ Wow, that was easy. Now it's your turn. Building something cool! +

+
+ ); +} \ No newline at end of file diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..485fd9e --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,18 @@ +import { Toaster } from "@/components/ui/sonner"; +import Navbar from "@/components/Navbar"; +import Sidebar from "@/components/Sidebar"; +export default async function AppLayout({ + children, +}: { + children: React.ReactNode; +}) { + return (
+ +
+ +{children} +
+
+ +
) +} \ No newline at end of file diff --git a/src/app/(app)/settings/page.tsx b/src/app/(app)/settings/page.tsx new file mode 100644 index 0000000..a801d9e --- /dev/null +++ b/src/app/(app)/settings/page.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useTheme } from "next-themes"; + +export default function Page() { + const { setTheme } = useTheme(); + return ( +
+

Settings

+
+
+

Appearance

+

+ Customize the appearance of the app. Automatically switch between + day and night themes. +

+
+ + + +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 875c01e..77d3522 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,33 +1,76 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; } } - -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; + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3314e47..03f7b34 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { ThemeProvider } from "@/components/ThemeProvider"; const inter = Inter({ subsets: ["latin"] }); @@ -16,7 +17,9 @@ export default function RootLayout({ }>) { return ( - {children} + +{children} + ); } diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..568eb6e --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,25 @@ +export default function Loading() { + return ( +
+
+ + Loading... +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index b81507d..0a69b41 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,113 +1,183 @@ -import Image from "next/image"; +/** + * v0 by Vercel. + * @see https://v0.dev/t/PmwTvNfrVgf + * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app + */ +import Link from "next/link"; -export default function Home() { +export default function LandingPage() { return ( -
-
-

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

-
- +
+ + + Acme Inc + +
-
- -
- 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! -

-
+ Features + + + Sign In + + + +
+
+
+
+
+
+
+

+ The complete platform
+ for building the Web +

+

+ Give your team the toolkit to stop configuring and start + innovating. Securely build, deploy, and scale the best web + experiences. +

+
+
+ + Get Started + + + Contact Sales + +
+
+
+
+
+
+
+
+
+
+ Key Features +
+

+ Faster iteration. More innovation. +

+

+ The platform for rapid progress. Let your team focus on + shipping features instead of managing infrastructure with + automated CI/CD. +

+
+
+
+
+
+
    +
  • +
    +

    Collaboration

    +

    + Make collaboration seamless with built-in code review + tools. +

    +
    +
  • +
  • +
    +

    Automation

    +

    + Automate your workflow with continuous integration. +

    +
    +
  • +
  • +
    +

    Scale

    +

    + Deploy to the cloud with a single click and scale with + ease. +

    +
    +
  • +
+
+
+
+
- -

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

-

- Explore starter templates for Next.js. -

-
+
+
+
+
+

+ Sign Up for Updates +

+

+ Stay updated with the latest product news and updates. +

+
+
+
+ + +
+
+
+
+
+
+
+

+ © 2024 Acme Inc. All rights reserved. +

+ +
+
+ ); +} - -

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

-

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

-
- -
+function MountainIcon(props: any) { + return ( + + + ); } diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..26b5257 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { usePathname } from "next/navigation"; + +import { Button } from "@/components/ui/button"; + +import { AlignRight } from "lucide-react"; +import { defaultLinks } from "@/config/nav"; + +export default function Navbar() { + const [open, setOpen] = useState(false); + const pathname = usePathname(); + return ( +
+ + {open ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..df3925a --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,16 @@ +import SidebarItems from "./SidebarItems"; + +const Sidebar = () => { + return ( + + ); +}; + +export default Sidebar; diff --git a/src/components/SidebarItems.tsx b/src/components/SidebarItems.tsx new file mode 100644 index 0000000..7dea48b --- /dev/null +++ b/src/components/SidebarItems.tsx @@ -0,0 +1,91 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { LucideIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { defaultLinks, additionalLinks } from "@/config/nav"; + +export interface SidebarLink { + title: string; + href: string; + icon: LucideIcon; +} + +const SidebarItems = () => { + return ( + <> + + {additionalLinks.length > 0 + ? additionalLinks.map((l) => ( + + )) + : null} + + ); +}; +export default SidebarItems; + +const SidebarLinkGroup = ({ + links, + title, + border, +}: { + links: SidebarLink[]; + title?: string; + border?: boolean; +}) => { + const fullPathname = usePathname(); + const pathname = "/" + fullPathname.split("/")[1]; + + return ( +
+ {title ? ( +

+ {title} +

+ ) : null} +
    + {links.map((link) => ( +
  • + +
  • + ))} +
+
+ ); +}; +const SidebarLink = ({ + link, + active, +}: { + link: SidebarLink; + active: boolean; +}) => { + return ( + +
+
+ + {link.title} +
+ + ); +}; diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..b0ff266 --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -0,0 +1,9 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { type ThemeProviderProps } from "next-themes/dist/types"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/src/components/ui/ThemeToggle.tsx b/src/components/ui/ThemeToggle.tsx new file mode 100644 index 0000000..63ff415 --- /dev/null +++ b/src/components/ui/ThemeToggle.tsx @@ -0,0 +1,40 @@ +"use client"; + +import * as React from "react"; +import { MoonIcon, SunIcon } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0ba4277 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +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 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", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + 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/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +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..677d05f --- /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/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..452f4d9 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/src/config/nav.ts b/src/config/nav.ts new file mode 100644 index 0000000..380f9a7 --- /dev/null +++ b/src/config/nav.ts @@ -0,0 +1,14 @@ +import { SidebarLink } from "@/components/SidebarItems"; +import { Cog, Globe, HomeIcon } from "lucide-react"; + +type AdditionalLinks = { + title: string; + links: SidebarLink[]; +}; + +export const defaultLinks: SidebarLink[] = [ + { href: "/dashboard", title: "Home", icon: HomeIcon }, + { href: "/settings", title: "Settings", icon: Cog }, +]; + +export const additionalLinks: AdditionalLinks[] = []; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts new file mode 100644 index 0000000..d0c05ff --- /dev/null +++ b/src/lib/db/index.ts @@ -0,0 +1,6 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { env } from "@/lib/env.mjs"; + +export const client = postgres(env.DATABASE_URL); +export const db = drizzle(client); \ No newline at end of file diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts new file mode 100644 index 0000000..03b3bb6 --- /dev/null +++ b/src/lib/db/migrate.ts @@ -0,0 +1,36 @@ +import { env } from "@/lib/env.mjs"; + +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; + + +const runMigrate = async () => { + if (!env.DATABASE_URL) { + throw new Error("DATABASE_URL is not defined"); + } + + +const connection = postgres(env.DATABASE_URL, { max: 1 }); + +const db = drizzle(connection); + + + console.log("⏳ Running migrations..."); + + const start = Date.now(); + + await migrate(db, { migrationsFolder: 'src/lib/db/migrations' }); + + const end = Date.now(); + + console.log("✅ Migrations completed in", end - start, "ms"); + + process.exit(0); +}; + +runMigrate().catch((err) => { + console.error("❌ Migration failed"); + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/src/lib/env.mjs b/src/lib/env.mjs new file mode 100644 index 0000000..6a0a110 --- /dev/null +++ b/src/lib/env.mjs @@ -0,0 +1,24 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + DATABASE_URL: z.string().min(1), + + }, + client: { + // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1), + }, + // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually + // runtimeEnv: { + // DATABASE_URL: process.env.DATABASE_URL, + // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + // }, + // For Next.js >= 13.4.4, you only need to destructure client variables: + experimental__runtimeEnv: { + // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + }, +}); diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..199deca --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,9 @@ +import { customAlphabet } from "nanoid"; +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789"); diff --git a/tailwind.config.ts b/tailwind.config.ts index e9a0944..061375e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,20 +1,76 @@ -import type { Config } from "tailwindcss"; +const { fontFamily } = require("tailwindcss/defaultTheme") -const config: Config = { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: ["src/app/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"], 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)", + }, + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + }, + 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")], +} diff --git a/tsconfig.json b/tsconfig.json index 7b28589..c5a7b07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,9 +22,20 @@ } ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "target": "esnext", + "baseUrl": "./" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file