feat: add user settings, sidebar hiding options, and quick links card

This commit is contained in:
Aidan 2025-03-03 20:15:36 -05:00
parent f659452169
commit 6f341bbd7a
10 changed files with 419 additions and 161 deletions

View File

@ -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**

View File

@ -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 (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
@ -23,51 +93,45 @@ export default function Settings() {
<h1 className="text-3xl font-bold mb-6 text-foreground">Settings</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<ChangePassword />
{/* DISABLED FOR NOW
<Card className="p-6">
<h2 className="text-xl font-semibold mb-4">UI Settings</h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="hide-ai">Hide Generative AI</Label>
<Switch id="hide-ai" />
<Switch
id="hide-ai"
checked={settings.hideGenAI}
disabled={loading}
onCheckedChange={(checked) => updateSetting('hideGenAI', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="hide-upgrades">Hide all upgrades/roles</Label>
<Switch id="hide-upgrades" />
<Switch
id="hide-upgrades"
checked={settings.hideUpgrades}
disabled={loading}
onCheckedChange={(checked) => updateSetting('hideUpgrades', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="hide-crypto">Hide crypto exchange</Label>
<Switch id="hide-crypto" />
<Switch
id="hide-crypto"
checked={settings.hideCrypto}
disabled={loading}
onCheckedChange={(checked) => updateSetting('hideCrypto', checked)}
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-xl font-semibold mb-4">Notifications</h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="hide-ai">Enable Notification System</Label>
<Switch id="hide-ai" />
</div>
<div className="flex items-center justify-between">
<Label htmlFor="hide-upgrades">Browser Notifications (coming soon)</Label>
<Switch id="hide-upgrades" disabled />
</div>
<div className="flex items-center justify-between">
<Label htmlFor="hide-crypto">Hide crypto exchange</Label>
<Switch id="hide-crypto" />
</div>
</div>
</Card>
*/}
</div>
</motion.div>
</div>
</main>
</div>
)
}
}

View File

@ -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 })

View File

@ -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}`, {

View File

@ -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 });
}
}

View File

@ -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<string | null>(null);
const [gitStatus, setGitStatus] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isAdmin, setIsAdmin] = useState(false)
const [error, setError] = useState<string | null>(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 = () => {
</div>
</CardContent>
</Card>
);
};
)
}

View File

@ -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 (
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Quick Links</CardTitle>
</CardHeader>
<CardContent>
<Link
href="mailto:support@librecloud.cc"
>
<Button
className="w-full mb-2"
>
<Headset />
Support
</Button>
</Link>
<Link
href="https://mail.librecloud.cc"
>
<Button
className="w-full mb-2"
>
<Mail />
Webmail
</Button>
</Link>
</CardContent>
</Card>
);
};

View File

@ -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 = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<WelcomeCard />
{/* TODO: Disabled for later - <DiskUsage />*/}
<LinkedAccounts />
<QuickLinks />
</div>
)

View File

@ -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 = () => (
<div className="fixed left-0 top-16 h-[calc(100vh-4rem)] w-64 border-r bg-background z-10 hidden lg:block">
<Sidebar className="h-full pt-16">
<SidebarContent className="h-full bg-background">
<SidebarGroup>
<SidebarGroupLabel>Services</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{workspaceGroupSidebarItems.map((item) => (
<SidebarMenuItem key={item.title}>
return (
<div className="fixed left-0 top-16 h-[calc(100vh-4rem)] w-64 border-r bg-background z-10 hidden lg:block">
<Sidebar className="h-full pt-16">
<SidebarContent className="h-full bg-background">
<SidebarGroup>
<SidebarGroupLabel>Services</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
<Link href="/account/dashboard">
<LayoutDashboard />
<span>Dashboard</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Tools</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{toolsGroupSidebarItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Account</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{accountGroupSidebarItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
<LogoutMenuItem />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</div>
)
{isLoading ? (
<SidebarMenuItem>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
) : (
!hideGenAI && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/account/dashboard/ai">
<Sparkle />
<span>Generative AI</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/account/dashboard/downloads">
<HardDriveDownload />
<span>Download Center</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Tools</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{isLoading ? (
<SidebarMenuItem>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
) : (
!hideCrypto && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/account/dashboard/exchange">
<Bitcoin />
<span>Exchange Crypto</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/account/dashboard/statistics">
<BarChartIcon />
<span>Statistics</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Account</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{isLoading ? (
<SidebarMenuItem>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
) : (
!hideUpgrades && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/account/dashboard/upgrades">
<Crown />
<span>Upgrades</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="mailto:support@librecloud.cc">
<Headset />
<span>Support</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/account/dashboard/settings">
<Settings />
<span>Settings</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<LogoutMenuItem />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</div>
)
}

View File

@ -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
}