diff --git a/README.md b/README.md index b03d172..e54a01b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ A Docker setup requires both Docker *and* Docker Compose. Following the environment variables section of this README, update your newly created `.env.local` file with your configuration. -5. **Initialize Prisma (optional)** +5. **Initialize Prisma** Because `web` uses a database for storing Git link statuses (and other things to come), you will need to initialize the SQLite database. @@ -59,6 +59,7 @@ A Docker setup requires both Docker *and* Docker Compose. ```bash bunx prisma migrate dev --name init + ``` 6. **Bring the container up** diff --git a/app/account/dashboard/settings/page.tsx b/app/account/dashboard/settings/page.tsx index 215ceb0..d9dd96b 100644 --- a/app/account/dashboard/settings/page.tsx +++ b/app/account/dashboard/settings/page.tsx @@ -2,10 +2,11 @@ import { motion } from "framer-motion" import { SideMenu } from "@/components/pages/dashboard/SideMenu" -//import { Switch } from "@/components/ui/switch" -//import { Label } from "@/components/ui/label" -//import { Card } from "@/components/ui/card" +import { Switch } from "@/components/ui/switch" +import { Label } from "@/components/ui/label" +import { Card } from "@/components/ui/card" import { ChangePassword } from "@/components/cards/dashboard/Settings/ChangePassword" +import { useState, useEffect } from "react"; const fadeIn = { initial: { opacity: 0, y: 20 }, @@ -14,6 +15,75 @@ const fadeIn = { } export default function Settings() { + const [settings, setSettings] = useState({ + hideGenAI: false, + hideUpgrades: false, + hideCrypto: false + }); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchSettings = async () => { + try { + setLoading(true); + const response = await fetch('/api/users/settings'); + if (response.ok) { + const data = await response.json(); + setSettings(data); + } else { + console.error('[!] Failed to fetch settings'); + } + } catch (error) { + console.error('[!] Error fetching settings:', error); + } finally { + setLoading(false); + } + }; + + fetchSettings(); + }, []); + + const updateSetting = async (settingName: string, value: boolean) => { + setSettings(prev => ({ + ...prev, + [settingName]: value + })); + + try { + setLoading(true); + const response = await fetch('/api/users/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...settings, + [settingName]: value + }), + }); + + if (response.ok) { + const updatedSettings = await response.json(); + setSettings(updatedSettings); + } else { + console.error('[!] Failed to update settings'); + setSettings(prev => ({ + ...prev, + [settingName]: !value + })); + } + } catch (error) { + console.error('[!] Error updating settings:', error); + setSettings(prev => ({ + ...prev, + [settingName]: !value + })); + } finally { + setLoading(false); + window.location.reload(); + } + }; + return (
@@ -23,51 +93,45 @@ export default function Settings() {

Settings

- {/* DISABLED FOR NOW

UI Settings

- + updateSetting('hideGenAI', checked)} + />
- + updateSetting('hideUpgrades', checked)} + />
- + updateSetting('hideCrypto', checked)} + />
- - -

Notifications

-
-
- - -
- -
- - -
- -
- - -
-
-
- */}
) -} \ No newline at end of file +} + diff --git a/app/api/git/link/route.ts b/app/api/git/link/route.ts index 25417e3..8387854 100644 --- a/app/api/git/link/route.ts +++ b/app/api/git/link/route.ts @@ -34,18 +34,27 @@ export async function POST(request: Request) { } const dbUsrCheck = await prisma.user.findUnique({ - where: { - email, - }, + where: { email }, }) if (dbUsrCheck) { - return NextResponse.json({ error: "Git account already linked" }, { status: 409 }) + if (dbUsrCheck.username) { + return NextResponse.json({ error: "Git account already linked" }, { status: 409 }) + } else { + await prisma.user.update({ + where: { email }, + data: { username }, + }) + return NextResponse.json({ success: true }) + } } else { await prisma.user.create({ data: { email, username, + hideGenAI: false, + hideUpgrades: false, + hideCrypto: false, }, }) return NextResponse.json({ success: true }) diff --git a/app/api/git/user/route.ts b/app/api/git/user/route.ts index af901b6..8cc1bbd 100644 --- a/app/api/git/user/route.ts +++ b/app/api/git/user/route.ts @@ -18,6 +18,8 @@ export async function GET() { if (!dbUser) { return NextResponse.json({ message: "User not found in database" }) + } else if (dbUser.username === null) { + return NextResponse.json({ error: "Git account not linked", dismissErr: true }, { status: 404 }) } const response = await fetch(`${process.env.GITEA_API_URL}/users/${dbUser.username}`, { diff --git a/app/api/users/settings/route.ts b/app/api/users/settings/route.ts new file mode 100644 index 0000000..09b0757 --- /dev/null +++ b/app/api/users/settings/route.ts @@ -0,0 +1,75 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; +import * as z from "zod"; + +const SettingsSchema = z.object({ + hideGenAI: z.boolean(), + hideUpgrades: z.boolean(), + hideCrypto: z.boolean(), +}); + +export async function GET() { + const session = await auth(); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { email } = session.user; + + const user = await prisma.user.findUnique({ where: { email } }); + if (user) { + return NextResponse.json({ + hideGenAI: user.hideGenAI, + hideUpgrades: user.hideUpgrades, + hideCrypto: user.hideCrypto, + }); + } else { + const newUser = await prisma.user.create({ data: { email } }); + return NextResponse.json({ + hideGenAI: newUser.hideGenAI, + hideUpgrades: newUser.hideUpgrades, + hideCrypto: newUser.hideCrypto, + }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized - Please login first" }, { status: 401 }); + } + const jsonData = await request.json(); + const parsedSettings = SettingsSchema.safeParse(jsonData); + if (!parsedSettings.success) { + return NextResponse.json({ error: "Invalid settings" }, { status: 400 }); + } + const { hideGenAI, hideUpgrades, hideCrypto } = parsedSettings.data; + const { email } = session.user; + + const user = await prisma.user.findUnique({ where: { email } }); + if (user) { + const updatedUser = await prisma.user.update({ + where: { email }, + data: { hideGenAI, hideUpgrades, hideCrypto }, + }); + return NextResponse.json({ + hideGenAI: updatedUser.hideGenAI, + hideUpgrades: updatedUser.hideUpgrades, + hideCrypto: updatedUser.hideCrypto, + }); + } else { + const newUser = await prisma.user.create({ + data: { email, hideGenAI, hideUpgrades, hideCrypto }, + }); + return NextResponse.json({ + hideGenAI: newUser.hideGenAI, + hideUpgrades: newUser.hideUpgrades, + hideCrypto: newUser.hideCrypto, + }); + } + } catch (error) { + console.error("[!] Error processing settings update:", error); + return NextResponse.json({ error: "Failed to update settings" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/components/cards/dashboard/overview/LinkedAccounts.tsx b/components/cards/dashboard/overview/LinkedAccounts.tsx index c1157dd..d31bc60 100644 --- a/components/cards/dashboard/overview/LinkedAccounts.tsx +++ b/components/cards/dashboard/overview/LinkedAccounts.tsx @@ -4,10 +4,10 @@ import { Badge } from "@/components/ui/badge"; import { Loader2 } from "lucide-react"; export const LinkedAccounts = () => { - const [gitStatus, setGitStatus] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isAdmin, setIsAdmin] = useState(false); - const [error, setError] = useState(null); + const [gitStatus, setGitStatus] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [isAdmin, setIsAdmin] = useState(false) + const [error, setError] = useState(null) useEffect(() => { const fetchGitStatus = async () => { @@ -16,17 +16,20 @@ export const LinkedAccounts = () => { const response = await fetch("/api/git/user") const data = await response.json() if (!response.ok) { - if (data.error) { + if (data.error && !data.dismissErr) { throw new Error(data.error) - } else { + } else if (response.status !== 404) { throw new Error(`HTTP error: ${response.status}`) + } else { + console.log(data.error) + } + } else { + if (data.is_admin) { + setIsAdmin(true) + } + if (!data.message) { + setGitStatus(true) } - } - if (data.is_admin) { - setIsAdmin(true) - } - if (!data.message) { - setGitStatus(true) } } catch (err: unknown) { if (err instanceof Error) { @@ -81,5 +84,5 @@ export const LinkedAccounts = () => { - ); -}; \ No newline at end of file + ) +} \ No newline at end of file diff --git a/components/cards/dashboard/overview/QuickLinks.tsx b/components/cards/dashboard/overview/QuickLinks.tsx new file mode 100644 index 0000000..6e540cb --- /dev/null +++ b/components/cards/dashboard/overview/QuickLinks.tsx @@ -0,0 +1,39 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { + Mail, + Headset +} from "lucide-react" +import Link from "next/link" + +export const QuickLinks = () => { + return ( + + + Quick Links + + + + + + + + + + + ); +}; diff --git a/components/pages/dashboard/OverviewTab.tsx b/components/pages/dashboard/OverviewTab.tsx index 72688e4..cee8dd1 100644 --- a/components/pages/dashboard/OverviewTab.tsx +++ b/components/pages/dashboard/OverviewTab.tsx @@ -1,12 +1,14 @@ //import { DiskUsage } from "@/components/cards/dashboard/DiskUsage" import { WelcomeCard } from "@/components/cards/dashboard/overview/WelcomeCard" import { LinkedAccounts } from "@/components/cards/dashboard/overview/LinkedAccounts" +import { QuickLinks } from "@/components/cards/dashboard/overview/QuickLinks" export const OverviewTab = () => (
{/* TODO: Disabled for later - */} +
) diff --git a/components/pages/dashboard/SideMenu.tsx b/components/pages/dashboard/SideMenu.tsx index 8844a49..b9fd157 100644 --- a/components/pages/dashboard/SideMenu.tsx +++ b/components/pages/dashboard/SideMenu.tsx @@ -1,116 +1,177 @@ -import { LayoutDashboard, Crown, Settings, Sparkle, HardDriveDownload, Bitcoin, Headset, ChartSpline } from "lucide-react" -import { Sidebar, SidebarMenuButton, SidebarGroup, SidebarContent, SidebarMenu, SidebarGroupContent, SidebarGroupLabel, SidebarMenuItem } from "@/components/ui/sidebar" +"use client" + +import { + LayoutDashboard, + Crown, + Settings, + Sparkle, + HardDriveDownload, + Bitcoin, + Headset, + BarChartIcon, +} from "lucide-react" +import { + Sidebar, + SidebarMenuButton, + SidebarGroup, + SidebarContent, + SidebarMenu, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenuItem, + SidebarMenuSkeleton, +} from "@/components/ui/sidebar" import LogoutMenuItem from "@/components/custom/LogoutMenuItem" -import React from "react" -import Link from "next/link"; +import type React from "react" +import Link from "next/link" +import { useState, useEffect } from "react" -const workspaceGroupSidebarItems = [ - { - title: "Dashboard", - url: "/account/dashboard", - icon: LayoutDashboard, - }, - { - title: "Generative AI", - url: "/account/dashboard/ai", - icon: Sparkle, - }, - { - title: "Download Center", - url: "/account/dashboard/downloads", - icon: HardDriveDownload, - }, -] +export const SideMenu: React.FC = () => { + const [hideGenAI, setHideGenAI] = useState(true) + const [hideUpgrades, setHideUpgrades] = useState(true) + const [hideCrypto, setHideCrypto] = useState(true) + const [isLoading, setIsLoading] = useState(true) -const toolsGroupSidebarItems = [ - { - title: "Exchange Crypto", - url: "/account/dashboard/exchange", - icon: Bitcoin, - }, - { - title: "Statistics", - url: "/account/dashboard/statistics", - icon: ChartSpline, - }, -] + useEffect(() => { + fetch("/api/users/settings") + .then((res) => res.json()) + .then((data) => { + setHideGenAI(data.hideGenAI) + setHideUpgrades(data.hideUpgrades) + setHideCrypto(data.hideCrypto) + setIsLoading(false) + }) + .catch((error) => { + console.error("Failed to fetch user settings:", error) + setIsLoading(false) + }) + }, []) -const accountGroupSidebarItems = [ - { - title: "Upgrades", - url: "/account/dashboard/upgrades", - icon: Crown, - }, - { - title: "Support", - url: "https://t.me/nerdsorg_talk", - icon: Headset, - }, - { - title: "Settings", - url: "/account/dashboard/settings", - icon: Settings, - } -] - -export const SideMenu: React.FC = () => ( -
- - - - Services - - - {workspaceGroupSidebarItems.map((item) => ( - + return ( +
+ + + + Services + + + - - - {item.title} + + + Dashboard - ))} - - - - - Tools - - - {toolsGroupSidebarItems.map((item) => ( - - - - - {item.title} - - - - ))} - - - - - Account - - - {accountGroupSidebarItems.map((item) => ( - - - - - {item.title} - - - - ))} - - - - - - -
-) + + {isLoading ? ( + + + + ) : ( + !hideGenAI && ( + + + + + Generative AI + + + + ) + )} + + + + + + Download Center + + + +
+
+
+ + + Tools + + + {isLoading ? ( + + + + ) : ( + !hideCrypto && ( + + + + + Exchange Crypto + + + + ) + )} + + + + + + Statistics + + + + + + + + + Account + + + {isLoading ? ( + + + + ) : ( + !hideUpgrades && ( + + + + + Upgrades + + + + ) + )} + + + + + + Support + + + + + + + + + Settings + + + + + + + + +
+
+
+ ) +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9b76aa..4d70715 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,10 +8,12 @@ datasource db { } model User { - id String @id @default(cuid()) - email String @unique - username String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + email String @unique + username String? @unique + hideGenAI Boolean @default(false) + hideUpgrades Boolean @default(false) + hideCrypto Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } -