diff --git a/package.json b/package.json index a97427d..92c6e45 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "blur": "yarn dlx tsx ./processImages.ts" }, "dependencies": { - "@mdx-js/loader": "^3.0.1", - "@mdx-js/react": "^3.0.1", + "@mdx-js/loader": "^3.1.0", + "@mdx-js/react": "^3.1.0", "@next/mdx": "^15.0.0", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", @@ -22,6 +22,7 @@ "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", @@ -29,9 +30,9 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.36.0", "jose": "^5.6.3", - "lucide-react": "^0.427.0", + "lucide-react": "^0.454.0", "next": "^15.0.2", - "next-themes": "^0.3.0", + "next-themes": "^0.4.1", "postgres": "^3.4.5", "react": "19.0.0-rc-7c8e5e7a-20241101", "react-dom": "19.0.0-rc-7c8e5e7a-20241101", @@ -41,7 +42,7 @@ }, "devDependencies": { "@faker-js/faker": "^9.0.0", - "@tailwindcss/postcss": "^4.0.0-alpha.17", + "@tailwindcss/postcss": "^4.0.0-alpha.31", "@types/mdx": "^2.0.13", "@types/node": "^20", "@types/react": "npm:types-react@19.0.0-rc.1", @@ -53,7 +54,7 @@ "eslint-plugin-react-compiler": "19.0.0-beta-8a03594-20241020", "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", - "tailwindcss": "^4.0.0-alpha.17", + "tailwindcss": "^4.0.0-alpha.31", "tsx": "^4.19.2", "typescript": "^5.5.3" }, diff --git a/processImages.ts b/processImages.ts deleted file mode 100644 index 188fbea..0000000 --- a/processImages.ts +++ /dev/null @@ -1,89 +0,0 @@ -import fs from "fs"; -import path from "path"; -import sharp from "sharp"; - -type imgMeta = { - fileName: string; - relativePath: string; - width: number; - height: number; - imgBase64: string; -}; - -async function processImage(imagePath: string) { - const sharpImg = sharp(imagePath); - const meta = await sharpImg.metadata(); - if (!meta) { - return null; - } - const placeholderImgWidth = 20; - const imgAspectRatio = meta.width! / meta.height!; - const placeholderImgHeight = Math.round(placeholderImgWidth / imgAspectRatio); - const imgBase64 = await sharpImg - .resize(placeholderImgWidth, placeholderImgHeight) - .toBuffer() - .then( - (buffer) => - `data:image/${meta.format};base64,${buffer.toString("base64")}`, - ); - - return { - fileName: path.basename(imagePath), - // Strip public prefix, /public is / in Nextjs runtime environment - relativePath: path - .relative(process.cwd(), imagePath) - .substring("public".length), - width: meta.width!, - height: meta.height!, - imgBase64, - }; -} -async function processImages(folderName: string, recursive: boolean) { - const imageFolder = fs.readdirSync(folderName); - - const recurseFolders = []; - const folderImgMeta = {} as { [key: string]: imgMeta }; - - for await (const item of imageFolder) { - const itemIsDir = fs.lstatSync(path.join(folderName, item)).isDirectory(); - if (itemIsDir) { - recurseFolders.push(path.join(folderName, item)); - } else if (path.extname(item) !== ".json") { - const imgMeta = await processImage(path.join(folderName, item)); - folderImgMeta[imgMeta!.fileName] = imgMeta!; - } - } - - fs.writeFileSync( - path.join(folderName, "imgMeta.json"), - JSON.stringify(folderImgMeta), - ); - - if (Object.keys(folderImgMeta).length !== 0) { - const constantsFilePath = path.join( - process.cwd(), - "/src", - "lib", - "imgMeta.ts", - ); - - const constName = folderName.split("/").slice(-1)[0]; - const fileContent = `export const ${constName} = ${JSON.stringify(folderImgMeta)};`; - fs.appendFileSync(constantsFilePath, fileContent); - } - - if (recursive) - recurseFolders.forEach(async (folder) => await processImages(folder, true)); - - return; -} - -async function processAllImages() { - const imgMetaFilePath = path.join(process.cwd(), "/src", "lib", "imgMeta.ts"); - if (fs.existsSync(imgMetaFilePath)) { - fs.rmSync(imgMetaFilePath); - } - await processImages(path.join(process.cwd(), "/public", "assets"), true); -} - -processAllImages(); diff --git a/src/app/forum/components/Header.tsx b/src/app/forum/components/Header.tsx new file mode 100644 index 0000000..76cf894 --- /dev/null +++ b/src/app/forum/components/Header.tsx @@ -0,0 +1,10 @@ +import Auth from "@/components/auth"; + +export default async function Header() { + return ( +
+

Forum

+ +
+ ); +} diff --git a/src/app/forum/components/SidePanel.tsx b/src/app/forum/components/SidePanel.tsx new file mode 100644 index 0000000..db5e4a9 --- /dev/null +++ b/src/app/forum/components/SidePanel.tsx @@ -0,0 +1,67 @@ +import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; + +// Menu items. +const items = [ + { + title: "Home", + url: "#", + icon: Home, + }, + { + title: "Inbox", + url: "#", + icon: Inbox, + }, + { + title: "Calendar", + url: "#", + icon: Calendar, + }, + { + title: "Search", + url: "#", + icon: Search, + }, + { + title: "Settings", + url: "#", + icon: Settings, + }, +]; + +export function SidePanel() { + return ( + + + + Application + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + ); +} diff --git a/src/app/forum/error.tsx b/src/app/forum/error.tsx new file mode 100644 index 0000000..9440da8 --- /dev/null +++ b/src/app/forum/error.tsx @@ -0,0 +1,30 @@ +"use client"; // Error components must be Client Components + +import { useEffect } from "react"; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error(error); + }, [error]); + + return ( +
+

Something went wrong!

+ +
+ ); +} diff --git a/src/app/forum/home/page.tsx b/src/app/forum/home/page.tsx index 4032352..1c31b76 100644 --- a/src/app/forum/home/page.tsx +++ b/src/app/forum/home/page.tsx @@ -1,3 +1,23 @@ -export default function Home() { - return null; +import { dbClient } from "@/db/forum/db"; +import { messages } from "@/db/forum/schema"; +import { desc, isNull } from "drizzle-orm"; + +export default async function Home() { + const posts = await dbClient() + .db.select() + .from(messages) + .where(isNull(messages.parent_id)) + .orderBy(desc(messages.createdAt)); + return ( +
+ {posts.map((post) => ( +
+ {post.message} +
+
+
+
+ ))} +
+ ); } diff --git a/src/app/forum/layout.tsx b/src/app/forum/layout.tsx index 5788c57..c6cf2a2 100644 --- a/src/app/forum/layout.tsx +++ b/src/app/forum/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; - +import Header from "./components/Header"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { SidePanel } from "./components/SidePanel"; export const metadata: Metadata = { title: "Forum", description: "Created by DJ Sisson", @@ -10,5 +12,21 @@ export default function AsteroidzLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return children; + return ( +
+ + +
+
+ +
+
+ +
+
{children}
+
+
+
+
+ ); } diff --git a/src/app/forum/page.tsx b/src/app/forum/page.tsx index cb4db69..20da874 100644 --- a/src/app/forum/page.tsx +++ b/src/app/forum/page.tsx @@ -1,3 +1,5 @@ -export default function Forum() { - return null; +import { redirect } from "next/navigation"; + +export default function Home() { + return
{redirect("/home")}
; } diff --git a/src/app/forum/template.tsx b/src/app/forum/template.tsx new file mode 100644 index 0000000..9aceb93 --- /dev/null +++ b/src/app/forum/template.tsx @@ -0,0 +1,3 @@ +export default function Template({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/src/app/global.css b/src/app/global.css index 0ec2e43..c8d5a7c 100755 --- a/src/app/global.css +++ b/src/app/global.css @@ -28,8 +28,15 @@ --animate-appear-up: animate-appear-up 500ms ease-in-out; --animate-slide-in: animate-slide-in 500ms ease-in-out; --font-family-sans: var(--font-inter), sans-serif; + --color-sidebar: hsl(var(--sidebar-background)); + --color-sidebar-foreground: hsl(var(--sidebar-foreground)); + --color-sidebar-primary: hsl(var(--sidebar-primary)); + --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); + --color-sidebar-accent: hsl(var(--sidebar-accent)); + --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); + --color-sidebar-border: hsl(var(--sidebar-border)); + --color-sidebar-ring: hsl(var(--sidebar-ring)); } - :root { color-scheme: light dark; --clr-1: #0e4b50; @@ -71,6 +78,14 @@ --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { @@ -98,6 +113,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } @@ -109,6 +132,7 @@ @apply bg-background text-foreground; } } + @keyframes rainbow-scroll { 0% { background-position: 0% 50%; diff --git a/src/app/page.tsx b/src/app/page.tsx index 17f17e2..3ac0a2a 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -23,6 +23,11 @@ export default function Home() { Tilez +
  • + + Forum + +
  • ); diff --git a/src/app/redirect/[[...redirect]]/route.ts b/src/app/redirect/[[...redirect]]/route.ts index d168cbc..ed4bf4f 100755 --- a/src/app/redirect/[[...redirect]]/route.ts +++ b/src/app/redirect/[[...redirect]]/route.ts @@ -7,7 +7,7 @@ export async function GET( props: { params: Promise<{ redirect?: string[] }> }, ) { const params = await props.params; - const { searchParams, hostname, protocol } = new URL(request.url); + const { searchParams, hostname, protocol, port } = new URL(request.url); const code = searchParams.get("code"); const redirectUrl = `/${params.redirect?.join("/") ?? ""}`; @@ -19,7 +19,9 @@ export async function GET( const isLocalEnv = process.env.NODE_ENV === "development"; if (isLocalEnv) { // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host - return NextResponse.redirect(`${protocol}//${hostname}${redirectUrl}`); + return NextResponse.redirect( + `${protocol}//${hostname}:${port}${redirectUrl}`, + ); } else if (forwardedHost) { return NextResponse.redirect(`https://${forwardedHost}${redirectUrl}`); } else { diff --git a/src/components/auth.tsx b/src/components/auth.tsx index d965ab9..27e75a9 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -65,6 +65,7 @@ export default async function Auth({ app }: { app: string }) { xmlns="http://www.w3.org/2000/svg" fill="white" color="#181717" + style={{ width: "32px", height: "32px" }} > GitHub @@ -81,8 +82,7 @@ export default async function Auth({ app }: { app: string }) { >
    ( + undefined, + ); + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + return !!isMobile; +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..44b10a9 --- /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/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..3b9f2b8 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..d5b78bf --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client"; + +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + }, +); + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +); +SheetHeader.displayName = "SheetHeader"; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +); +SheetFooter.displayName = "SheetFooter"; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..867be8b --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,772 @@ +"use client"; + +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps, cva } from "class-variance-authority"; +import { PanelLeft } from "lucide-react"; + +import { useIsMobile } from "@/components/hooks/use-mobile"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +const SIDEBAR_COOKIE_NAME = "sidebar:state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContext = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref, + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + ], + ); + + return ( + + +
    + {children} +
    +
    +
    + ); + }, +); +SidebarProvider.displayName = "SidebarProvider"; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref, + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === "none") { + return ( +
    + {children} +
    + ); + } + + if (isMobile) { + return ( + + + +
    {children}
    +
    +
    + ); + } + + return ( +
    + {/* This is what handles the sidebar gap on desktop */} +
    + +
    + ); + }, +); +Sidebar.displayName = "Sidebar"; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( +