i don't even know at this point (3 billion changes to build the first release)
All checks were successful
Build and Push Nightly CI Image / build_and_push (push) Successful in 1m47s
Build and Push Docker Image / build_and_push (push) Successful in 3s

This commit is contained in:
Aidan 2025-02-16 15:28:17 -05:00
parent e8927986d5
commit 56603e7e99
77 changed files with 4151 additions and 839 deletions

28
.gitea/workflows/ci.yaml Normal file
View File

@ -0,0 +1,28 @@
name: Build and Push Nightly CI Image
on:
schedule:
- cron: '0 0 * * *'
push:
branches:
- main
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Log in to Gitea Package Registry
run: echo "${{ secrets.PACKAGE_TOKEN }}" | docker login $SERVER_URL -u $USERNAME --password-stdin
env:
SERVER_URL: ${{ secrets.SERVER_URL }}
USERNAME: ${{ secrets.USERNAME }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
- name: Build Docker Image (ci tag)
run: docker build -t git.pontusmail.org/librecloud/web:ci .
- name: Push Docker Image (ci tag)
run: docker push git.pontusmail.org/librecloud/web:ci

10
.gitignore vendored
View File

@ -43,5 +43,13 @@ next-env.d.ts
# vs code # vs code
.vscode/ .vscode/
# idea
.idea/
# package-lock # package-lock
package-lock.json package-lock.json
bun.lockb
# prisma
prisma/dev.db
prisma/migrations/

View File

@ -4,6 +4,7 @@ COPY package.json ./
COPY bun.lockb ./ COPY bun.lockb ./
RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
COPY . . COPY . .
RUN bunx prisma migrate dev --name init
RUN bun run build RUN bun run build
EXPOSE 3000 EXPOSE 3000
CMD ["bun", "start"] CMD ["bun", "start"]

View File

@ -1,10 +1,25 @@
# web # <center>web</center>
LibreCloud's website ![Last Update](https://img.shields.io/badge/last_update-16_Feb_2024-blue)
[![License: CC0-1.0](https://img.shields.io/badge/License-CC0_1.0-lightgrey.svg)](http://creativecommons.org/publicdomain/zero/1.0/)
[![Build Status](https://git.pontusmail.org/librecloud/web/actions/workflows/docker.yaml/badge.svg)](https://git.pontusmail.org/librecloud/web/actions/?workflow=docker.yaml)
[![Build Status](https://git.pontusmail.org/librecloud/web/actions/workflows/ci.yaml/badge.svg)](https://git.pontusmail.org/librecloud/web/actions/?workflow=ci.yaml)
LibreCloud's website, dashboard, and API
## Docker Instructions ## Docker Instructions
1. **Fetch needed file(s)** A Docker setup requires both Docker *and* Docker Compose.
1. **Install Bun if you haven't already**
Bun is a fast JavaScript runtime, which we prefer over `npm`. These instructions will be written for Bun, but could be adapted to `npm` or `yarn` if needed.
```bash
curl -fsSL https://bun.sh/install | bash
```
2. **Fetch needed file(s)**
Pick your preferred option to get the file(s) needed for Docker. Either option is fine, although Git is arguably the best option. Pick your preferred option to get the file(s) needed for Docker. Either option is fine, although Git is arguably the best option.
@ -22,7 +37,7 @@ LibreCloud's website
You may have to install `wget`, or you could use `curl` instead. You may have to install `wget`, or you could use `curl` instead.
2. **Bring up the container** 3. **Bring the container up**
```bash ```bash
docker compose up -d docker compose up -d
@ -32,11 +47,22 @@ LibreCloud's website
You may customize the container with the included `docker-compose.yml` file if needed. Your server will start on port `3019` by default. We suggest using a reverse proxy to serve the site on a domain. You may customize the container with the included `docker-compose.yml` file if needed. Your server will start on port `3019` by default. We suggest using a reverse proxy to serve the site on a domain.
4. **Complete Setup**
If you would like to host the entire LibreCloud frontend and backend, you will also need to setup the following repositories and edit this project to work with *your* setup.
* [mail-connect](https://git.pontusmail.org/librecloud/mail-connect)
* [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver)
## Dev Server Instructions ## Dev Server Instructions
1. **Install Bun if you haven't already** 1. **Install Bun if you haven't already**
Bun is a fast JavaScript runtime, and is much faster than `npm`. These instructions will be written for Bun, but could be adapted to `npm` or `yarn` if needed. Bun is a fast JavaScript runtime, which we prefer over `npm`. These instructions will be written for Bun, but could be adapted to `npm` or `yarn` if needed.
```bash
curl -fsSL https://bun.sh/install | bash
```
2. **Clone the repo** 2. **Clone the repo**
@ -51,7 +77,19 @@ LibreCloud's website
bun install bun install
``` ```
4. **Start dev server** 4. **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.
A `schema.prisma` file has been provided to make this easy.
This can be done by executing:
```bash
bunx prisma migrate dev --name init
```
5. **Start dev server**
```bash ```bash
bun dev bun dev

7
actions/logout.ts Normal file
View File

@ -0,0 +1,7 @@
"use server";
import { signOut } from "@/auth";
export async function logout() {
await signOut();
}

View File

@ -0,0 +1,37 @@
"use client";
import { motion } from "framer-motion"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
import { Sparkles } from "lucide-react";
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 },
}
export default function GenAI() {
return (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
<main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<div className="container mx-auto px-4 py-6 w-full">
<motion.div {...fadeIn} className="text-center">
<Sparkles className="h-16 w-16 mx-auto mt-2" />
<h1 className="text-3xl font-bold mt-8 mb-3">Generative AI is coming soon</h1>
<p>Experience artificial intelligence without the bloat and cost.</p>
<ul className="mt-6 list-disc list-inside">
<li>Open-source (and public domain) chat interface</li>
<li>Use the same models you&apos;re familiar with</li>
<li>Pay per 1M tokens and save money</li>
<li>Free models for testing/use</li>
<li><span className="font-bold">ZERO</span> additional fees</li>
</ul>
<p className="mt-4">If you prefer not to see this service, you will be able to hide it from Settings when it launches.</p>
</motion.div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,51 @@
"use client";
import { motion } from "framer-motion"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { HomeTab } from "@/components/pages/dashboard/downloads/HomeTab"
import { EmailTab } from "@/components/pages/dashboard/downloads/EmailTab"
import { PassTab } from "@/components/pages/dashboard/downloads/PassTab"
import { GitTab } from "@/components/pages/dashboard/downloads/GitTab"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 },
}
export default function DownloadCenter() {
return (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
<main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<div className="container mx-auto px-4 py-6 w-full">
<motion.div {...fadeIn}>
<h1 className="text-3xl font-bold mb-6 text-foreground">Download Center</h1>
<Tabs defaultValue="home" className="w-full">
<TabsList className="mb-4 flex flex-wrap">
<TabsTrigger value="home">Home</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="pass">Pass</TabsTrigger>
<TabsTrigger value="git">Git</TabsTrigger>
</TabsList>
<TabsContent value="home">
<HomeTab />
</TabsContent>
<TabsContent value="email">
<EmailTab />
</TabsContent>
<TabsContent value="pass">
<PassTab />
</TabsContent>
<TabsContent value="git">
<GitTab />
</TabsContent>
</Tabs>
</motion.div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,28 @@
"use client";
import { motion } from "framer-motion"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 },
}
export default function Exchange() {
return (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
<main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<div className="container mx-auto px-4 py-6 w-full">
<motion.div {...fadeIn}>
<h1 className="text-3xl font-bold mb-6">Exchange Crypto</h1>
<p>Coming soon</p>
<p className="mt-4">If you prefer not to see this service, you will be able to hide it from Settings when it launches.</p>
</motion.div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,37 @@
import { type ReactNode } from "react"
import { ThemeProvider } from "@/components/theme-provider"
import SidebarToggle from "@/components/custom/SidebarToggle"
import { SidebarProvider } from "@/components/ui/sidebar"
import { Footer } from "@/components/pages/dashboard/Footer"
import { Header } from "@/components/pages/dashboard/Header";
const DashboardLayout = ({ children }: { children: ReactNode }) => {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="min-h-screen flex flex-col bg-background text-foreground">
<div className="flex-grow">
<SidebarProvider>
<div className="flex flex-col w-full min-h-screen bg-background">
<Header />
{children}
<div className="lg:ml-64">
<Footer />
</div>
</div>
<div className="fixed bottom-4 left-4">
<SidebarToggle />
</div>
</SidebarProvider>
</div>
</div>
</ThemeProvider>
)
}
export default DashboardLayout

View File

@ -1,14 +1,13 @@
"use client" "use client"
import Image from "next/image" import { useState, useEffect } from "react"
import Link from "next/link"
import { useEffect, useState } from "react"
import { Mail, GitBranch, Music, Key, CheckCircle, XCircle, User, LayoutDashboard, Settings, Briefcase, Loader } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "@/components/ui/sidebar"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { getCookie } from "cookies-next" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { OverviewTab } from "@/components/pages/dashboard/OverviewTab"
import { SecurityTab } from "@/components/pages/dashboard/SecurityTab"
import { ServicesTab } from "@/components/pages/dashboard/ServicesTab"
import { GitTab } from "@/components/pages/dashboard/GitTab"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
const fadeIn = { const fadeIn = {
initial: { opacity: 0, y: 20 }, initial: { opacity: 0, y: 20 },
@ -17,42 +16,48 @@ const fadeIn = {
} }
export default function Dashboard() { export default function Dashboard() {
const [diskUsage, setDiskUsage] = useState(0) const [dashboardState, setDashboardState] = useState({
const [isGitLinked, setIsGitLinked] = useState(false) gitUser: "Unlinked",
const [gitUser, setGitUser] = useState("Unlinked") gitFollowerCt: 0,
const [gitFollowerCt, setGitFollowerCt] = useState(0) gitFollowingCt: 0,
const [gitAvatar, setGitAvatar] = useState(null) gitAvatar: "",
const [gitProfileCardLoading, setGitProfileCardLoading] = useState(true) gitLastLogin: "",
const [gitProfileCardError, setGitProfileCardError] = useState(false) gitProfileCardLoading: true,
gitProfileCardError: false,
gitIsAdmin: false,
gitEmail: "",
showRunSecurityCheckBtn: true,
securityCheckBtnLoading: false,
})
useEffect(() => { useEffect(() => {
const checkGitLinkStatus = async () => { const checkGitLinkStatus = async () => {
try { try {
const key = getCookie("key") const response = await fetch("http://localhost:3000/api/git/user")
const email = getCookie("email")
const response = await fetch("http://localhost:3001/account/links", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ key, email, exi: true }),
})
if (response.ok) { if (response.ok) {
const data = await response.json() const data = await response.json()
setIsGitLinked(data.linked || false) setDashboardState((prev) => ({
setGitUser(data.user || "Unlinked") ...prev,
setGitAvatar(data.avatar || null) gitUser: data.username || "Unlinked",
setGitFollowerCt(data.followers || 0) gitAvatar: data.avatar_url || "",
setGitProfileCardLoading(false) gitLastLogin: data.last_login || "Never",
gitFollowerCt: data.followers_count || 0,
gitFollowingCt: data.following_count || 0,
gitIsAdmin: data.is_admin || false,
gitEmail: data.email || "",
gitProfileCardLoading: false,
}))
} else { } else {
setGitProfileCardError(true) throw new Error("Failed to fetch Gitea account details");
console.error("Failed to fetch Git link status")
} }
} catch (error) { } catch (error) {
setGitProfileCardError(true) console.error("Error fetching your Gitea user data:", error);
console.error("Error checking Git link status:", error) setDashboardState((prev) => ({
...prev,
gitProfileCardError: true,
gitProfileCardLoading: false,
}))
} }
} }
@ -60,163 +65,36 @@ export default function Dashboard() {
}, []) }, [])
return ( return (
<SidebarProvider> <div className="flex flex-1 overflow-hidden">
<div className="flex min-h-screen bg-background"> <SideMenu />
<Sidebar className="border-r w-64 flex-shrink-0"> <main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<SidebarHeader> <div className="container mx-auto px-4 py-6 w-full">
<div className="flex items-center space-x-2 px-4 py-2"> <motion.div {...fadeIn}>
<User className="h-6 w-6" /> <h1 className="text-3xl font-bold mb-6 text-foreground">Dashboard</h1>
<span className="font-bold">username</span> <Tabs defaultValue="overview" className="w-full">
</div> <TabsList className="mb-4 flex flex-wrap">
</SidebarHeader> <TabsTrigger value="overview">Overview</TabsTrigger>
<SidebarContent> <TabsTrigger value="security">Security</TabsTrigger>
<SidebarMenu> <TabsTrigger value="services">Services</TabsTrigger>
<SidebarMenuItem> <TabsTrigger value="git">Git</TabsTrigger>
<SidebarMenuButton className="w-full justify-start p-4 pt-8"> </TabsList>
<LayoutDashboard className="mr-2 h-4 w-4" /> <TabsContent value="overview">
Dashboard <OverviewTab />
</SidebarMenuButton> </TabsContent>
</SidebarMenuItem> <TabsContent value="security">
<SidebarMenuItem> <SecurityTab />
<SidebarMenuButton className="w-full justify-start p-4"> </TabsContent>
<Briefcase className="mr-2 h-4 w-4" /> <TabsContent value="services">
Services <ServicesTab />
</SidebarMenuButton> </TabsContent>
</SidebarMenuItem> <TabsContent value="git">
<SidebarMenuItem> <GitTab dashboardState={dashboardState} />
<SidebarMenuButton className="w-full justify-start p-4"> </TabsContent>
<User className="mr-2 h-4 w-4" /> </Tabs>
My Account </motion.div>
</SidebarMenuButton> </div>
</SidebarMenuItem> </main>
</SidebarMenu> </div>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton className="w-full justify-start">
<Settings className="mr-2 h-4 w-4" />
Settings
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
<main className="flex-1 overflow-y-auto bg-background">
<div className="mx-auto p-8 max-w-full">
<motion.div {...fadeIn}>
<h1 className="text-4xl font-bold mb-8">Dashboard</h1>
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<motion.div {...fadeIn} transition={{ delay: 0.1 }}>
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardHeader>
<CardTitle>Disk Usage</CardTitle>
</CardHeader>
<CardContent>
<Progress value={diskUsage} className="mb-2" />
<p className="text-sm text-muted-foreground">{diskUsage}% of 100GB used</p>
</CardContent>
</Card>
</motion.div>
<motion.div {...fadeIn} transition={{ delay: 0.3 }}>
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardHeader>
<CardTitle>Account Security</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center">
<CheckCircle className="text-green-500 mr-2" />
<span>Spam Protection</span>
</div>
<div className="flex items-center">
<XCircle className="text-destructive mr-2" />
<span>Two-Factor Authentication</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div {...fadeIn} transition={{ delay: 0.4 }}>
<Card className="hover:shadow-lg transition-shadow duration-300 h-full">
<CardHeader>
<CardTitle>Linked Accounts</CardTitle>
</CardHeader>
<CardContent className="flex flex-col h-full">
<div className="space-y-2">
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-green-500 mr-2" />
<span>p0ntus mail</span>
</div>
<div className="flex items-center">
<div className={`w-2 h-2 rounded-full ${isGitLinked ? "bg-green-500" : "bg-red-500"} mr-2`} />
<span>LibreCloud Git</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div {...fadeIn} transition={{ delay: 0.5 }}>
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardHeader>
<CardTitle>Services</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center">
<Mail className="text-primary mr-2" />
<Link href="https://user.pontusmail.org/">Mail</Link>
</div>
<div className="flex items-center">
<GitBranch className="text-primary mr-2" />
<Link href="https://git.librecloud.cc/">Git</Link>
</div>
<div className="flex items-center">
<Music className="text-muted-foreground mr-2" />
<Link href="https://music.librecloud.cc/">Music</Link>
</div>
<div className="flex items-center">
<Key className="text-muted-foreground mr-2" />
<Link href="https://vaultwarden.librecloud.cc/">Vaultwarden</Link>
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div {...fadeIn} transition={{ delay: 0.6 }}>
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardHeader>
<CardTitle>LibreCloud Git</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4">
<div className="w-16 h-16 rounded-full bg-secondary flex items-center justify-center">
{gitProfileCardLoading ? (
<div className="w-full h-full rounded-full flex items-center justify-center">
<Loader className="animate-spin h-8 w-8 text-secondary-foreground" />
</div>
) : gitAvatar ? (
<Image src={gitAvatar} alt="User Avatar" width={64} height={64} className="w-full h-full rounded-full object-cover" />
) : gitProfileCardError ? (
<User className="h-8 w-8 text-secondary-foreground" />
) : (
<User className="h-8 w-8 text-secondary-foreground" />
)}
</div>
<div>
<h3 className="font-semibold">{gitUser}</h3>
<p className="text-sm text-muted-foreground">{gitFollowerCt} {gitFollowerCt === 1 ? "follower" : "followers"}</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</motion.div>
</div>
</main>
</div>
</SidebarProvider>
) )
} }

View File

@ -0,0 +1,26 @@
"use client";
import { motion } from "framer-motion"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 },
}
export default function Settings() {
return (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
<main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<div className="container mx-auto px-4 py-6 w-full">
<motion.div {...fadeIn}>
<h1 className="text-3xl font-bold mb-6 text-foreground">Settings</h1>
<p>Coming soon</p>
</motion.div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,64 @@
"use client";
import { motion } from "framer-motion"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 },
}
export default function Statistics() {
return (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
<main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<div className="container mx-auto px-4 py-6 w-full">
<motion.div {...fadeIn}>
<h1 className="text-3xl font-bold mb-6 text-foreground">Statistics</h1>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Operational Costs</CardTitle>
<CardDescription>LibreCloud&#39;s monthly bill, for you to view.</CardDescription>
</CardHeader>
<CardContent>
<span className="text-sm"><span className="font-bold">Month of:</span> February</span>
<Separator className="my-4" />
<span className="font-bold">Server</span>
<div className="flex items-center justify-between mt-2">
<span className="text-sm">Main Server</span>
<span className="text-bold text-right">$28.88</span>
</div>
<Separator className="my-4" />
<span className="font-bold">Domains</span>
<div className="flex items-center justify-between mt-2">
<span className="text-sm">librecloud.cc</span>
<span className="text-bold text-right">$3.10</span>
</div>
<Separator className="my-4" />
<span className="font-bold">Addons</span>
<div className="flex items-center justify-between mt-2">
<span className="text-sm">+0GB Disk Space</span>
<span className="text-bold text-right">$0.00</span>
</div>
<div className="flex items-center justify-between mt-2">
<span className="text-sm">+0GB RAM</span>
<span className="text-bold text-right">$0.00</span>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between mt-2">
<span className="font-bold">TOTAL</span>
<span className="text-bold text-right">$31.98</span>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,27 @@
"use client";
import { motion } from "framer-motion"
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 },
}
export default function Upgrades() {
return (
<div className="flex flex-1 overflow-hidden">
<SideMenu />
<main className="flex-1 w-full overflow-y-auto pl-0 lg:pl-64">
<div className="container mx-auto px-4 py-6 w-full">
<motion.div {...fadeIn}>
<h1 className="text-3xl font-bold mb-6 text-foreground">Upgrades</h1>
<p>Coming soon</p>
</motion.div>
</div>
</main>
</div>
)
}

View File

@ -1,15 +1,10 @@
import { Theme } from "@radix-ui/themes"
import "@radix-ui/themes/styles.css"
export default function AccountLayout({ export default function AccountLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<Theme appearance="dark" accentColor="blue" grayColor="slate"> <div className="min-h-screen bg-gray-900">{children}</div>
<div className="min-h-screen bg-gray-900">{children}</div>
</Theme>
) )
} }

View File

@ -1,118 +0,0 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Key } from "lucide-react"
import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
export default function Login() {
const [magicCode, setMagicCode] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
useEffect(() => {
const validateStageTwo = async () => {
setErrorMessage("")
setIsLoading(true)
try {
const response = await fetch('http://localhost:3001/auth/validateStageTwo', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: Cookies.get("email"), stageTwoKey: Cookies.get("stageTwoKey") }),
})
const data = await response.json()
if (!response.ok || !data.success) {
router.push("/account/login")
}
} catch (error) {
console.error("There was a problem with checking the status of your request:", error)
setErrorMessage("An unexpected error occurred. Please try again.")
} finally {
setIsLoading(false)
}
}
validateStageTwo()
}, [magicCode, router])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setErrorMessage("")
try {
const response = await fetch('http://localhost:3001/auth/validateMagicCode', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: Cookies.get("email"), stageTwoKey: Cookies.get("stageTwoKey"), magicCode }),
})
const data = await response.json()
if (response.ok && data.success) {
Cookies.set("key", data.key)
Cookies.remove("stageTwoKey")
router.push("/account/dashboard")
} else {
setErrorMessage(data.error || "An unknown error occurred.")
}
} catch (error) {
console.error("There was a problem with checking the magic code:", error)
setErrorMessage("An unexpected error occurred. Please try again.")
} finally {
setIsLoading(false)
}
}
return (
<div className="flex h-screen items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Enter Your Magic Code</CardTitle>
<CardDescription>Check your email for the code we sent you.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Input
type="text"
placeholder="Magic Code"
value={magicCode}
onChange={(e) => setMagicCode(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
"Checking..."
) : (
<>
<Key className="mr-2 h-4 w-4" />
Submit
</>
)}
</Button>
{errorMessage && (
<Alert variant="destructive">
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
</form>
</CardContent>
</Card>
</div>
)
}

View File

@ -1,86 +1,38 @@
"use client"
import Link from "next/link" import Link from "next/link"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Mail } from "lucide-react"
import Cookies from "js-cookie"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { auth, signIn } from "@/auth"
import { Alert, AlertDescription } from "@/components/ui/alert" import { redirect } from "next/navigation";
import { SiAuthentik } from "react-icons/si"
export default function Login() { export default async function Login() {
const [email, setEmail] = useState("") const session = await auth()
const [errorMessage, setErrorMessage] = useState("")
const [isLoading, setIsLoading] = useState(false) if (session) {
const router = useRouter() return redirect("/account/dashboard")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setErrorMessage("")
try {
const response = await fetch('http://localhost:3001/auth/login', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
})
const data = await response.json()
if (response.ok && data.success) {
Cookies.set("stageTwoKey", data.stageTwoKey)
Cookies.set("email", email)
router.push("/account/login/code")
} else {
setErrorMessage(data.error || "An unknown error occurred.")
}
} catch (error) {
console.error("There was a problem with requesting a magic code:", error)
setErrorMessage("An unexpected error occurred. Please try again.")
} finally {
setIsLoading(false)
}
} }
return ( return (
<div className="flex h-screen items-center justify-center"> <div className="flex h-screen items-center justify-center">
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">
<CardHeader> <CardHeader>
<CardTitle>Log in to your account</CardTitle> <CardTitle>Log in to your account</CardTitle>
<CardDescription>If you still have a p0ntus mail account, select &#34;I don&apos;t have an account&#34;</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="space-y-4"> <form
<div className="space-y-2"> className="space-y-4"
<Input action={async () => {
type="email" "use server"
placeholder="Email" await signIn("authentik", { redirectTo: "/account/dashboard" })
value={email} }}
onChange={(e) => setEmail(e.target.value)} >
required <Button type="submit" className="w-full">
/> <SiAuthentik className="h-4 w-4" />
</div> Sign in with Authentik
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
"Sending..."
) : (
<>
<Mail className="mr-2 h-4 w-4" />
Send Magic Code
</>
)}
</Button> </Button>
{errorMessage && (
<Alert variant="destructive">
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<div className="text-center"> <div className="text-center">
<Link href="https://user.pontusmail.org/admin/user/signup" className="text-sm underline"> <Link href="/account/signup" className="text-sm underline">
I don&apos;t have an account I don&apos;t have an account
</Link> </Link>
</div> </div>

View File

@ -1,77 +0,0 @@
"use client"
import { useEffect } from "react"
import { Flex, Text, Spinner } from "@radix-ui/themes"
import { useRouter } from "next/navigation"
import Cookies from "js-cookie"
export default function Logout() {
const router = useRouter()
useEffect(() => {
const verifyKey = async () => {
try {
const response = await fetch('http://localhost:3001/auth/validateKey', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: Cookies.get("email"), key: Cookies.get("key") }),
})
const data = await response.json()
if (data.error) {
router.push('/account/login')
return false;
}
} catch (error) {
console.error("There was a problem with checking the existing key:", error);
return false;
}
}
const logoutUser = async () => {
const keycheck = await verifyKey()
if (keycheck !== false) {
try {
const response = await fetch('http://localhost:3001/auth/logout', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: Cookies.get("email"), key: Cookies.get("key") }),
})
const data = await response.json()
if (data.success) {
Cookies.remove("key")
Cookies.remove("email")
router.push("/account/login")
} else if (data.error) {
console.error("There was a problem with processing the logout on the backend:", data.error);
} else {
console.error("There was a problem with processing the logout on the backend:");
}
} catch (error) {
console.error("There was a problem with processing the logout on the backend:", error);
}
}
}
const timer = setTimeout(() => {
logoutUser()
}, 1250)
return () => clearTimeout(timer)
}, [router])
return (
<Flex direction="column" justify="center" align="center" className="h-screen" gap="4">
<Spinner size="3" />
<Text size="5">Logging out...</Text>
</Flex>
)
}

14
app/account/page.tsx Normal file
View File

@ -0,0 +1,14 @@
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function Page() {
const session = await auth()
if (session) {
redirect("/account/dashboard")
} else {
redirect("/account/login")
}
return null
}

View File

@ -1,38 +0,0 @@
"use client"
import { useState } from "react"
import { Flex, Text, Switch, Button } from "@radix-ui/themes"
import Sidebar from "../../components/account/Sidebar"
export default function Settings() {
const [darkMode, setDarkMode] = useState(true)
const [notifications, setNotifications] = useState(true)
return (
<Flex>
<Sidebar />
<Flex direction="column" className="flex-1 p-8">
<Text size="8" weight="bold" className="mb-6">
Settings
</Text>
<Flex direction="column" gap="4" className="mt-4" style={{ maxWidth: "500px" }}>
<Flex justify="between" align="center">
<Text size="4">Dark Mode</Text>
<Switch checked={darkMode} onCheckedChange={setDarkMode} />
</Flex>
<Flex justify="between" align="center" className="mb-4">
<Text size="4">Enable Notifications</Text>
<Switch checked={notifications} onCheckedChange={setNotifications} />
</Flex>
<Flex justify="center" gap="4" align="center">
<Button>Save Changes</Button>
<Button color="gray" variant="outline" highContrast>
Go Back
</Button>
</Flex>
</Flex>
</Flex>
</Flex>
)
}

365
app/account/signup/page.tsx Normal file
View File

@ -0,0 +1,365 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardFooter, CardTitle, CardDescription } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import Link from "next/link"
import { motion, AnimatePresence } from "framer-motion"
import { UserPlus, UserCog, Heart, AlertCircle, CheckCircle2, Mail, Lock, User } from "lucide-react"
import { useRouter } from "next/navigation"
import validator from "validator"
import PasswordValidator from "password-validator"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
export default function Signup() {
const router = useRouter()
const [formType, setFormType] = useState<"initial" | "create" | "migrate">("initial")
const [formData, setFormData] = useState({
name: "",
emailUsername: "",
emailDomain: "librecloud.cc",
password: "",
terms: false,
migratePassword: "",
migrateTerms: false,
migrateName: "",
})
const [isValid, setIsValid] = useState(false)
const [validationMessage, setValidationMessage] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)
const fadeInOut = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
transition: { duration: 0.3 },
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}))
}
useEffect(() => {
if (formType === "create") {
const { name, emailUsername, emailDomain, password, terms } = formData
const isNameValid = name.length >= 2
const isEmailValid = validateEmail(emailUsername, emailDomain)
const isPasswordValid = validatePassword(password)
setIsValid(isNameValid && isEmailValid && isPasswordValid && terms)
if (!isNameValid) setValidationMessage("Enter your name")
else if (!isEmailValid) setValidationMessage("Enter a valid email address")
else if (!isPasswordValid) setValidationMessage("Weak Password")
else if (!terms) setValidationMessage("Accept the terms")
else setValidationMessage("Create Account")
} else if (formType === "migrate") {
const { emailUsername, emailDomain, migratePassword, migrateTerms, migrateName } = formData
const isEmailValid = validateEmail(emailUsername, emailDomain)
const isPasswordValid = validatePassword(migratePassword)
const isNameValid = migrateName.length >= 2
setIsValid(isEmailValid && isPasswordValid && migrateTerms && isNameValid)
if (!isNameValid) setValidationMessage("Enter your name")
else if (!isEmailValid) setValidationMessage("Enter a valid email address")
else if (!isPasswordValid) setValidationMessage("Weak Password")
else if (!migrateTerms) setValidationMessage("Accept the terms")
else setValidationMessage("Migrate Account")
}
}, [formData, formType])
const getButtonIcon = () => {
if (isValid) return <CheckCircle2 className="mr-2 h-4 w-4" />
if (validationMessage.includes("name")) return <User className="mr-2 h-4 w-4" />
if (validationMessage.includes("email")) return <Mail className="mr-2 h-4 w-4" />
if (validationMessage.includes("Password")) return <Lock className="mr-2 h-4 w-4" />
if (validationMessage.includes("terms")) return <AlertCircle className="mr-2 h-4 w-4" />
return null
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const email = `${formData.emailUsername}@${formData.emailDomain}`
const response = await fetch("/api/users/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formType === "create" ? formData.name : formData.migrateName,
email: email,
password: formType === "create" ? formData.password : formData.migratePassword,
migrate: formType === "migrate",
}),
})
const data = await response.json()
if (data.success) {
router.push("/account/signup/success")
} else {
setValidationMessage(data.message || "Failed to create account. Please try again.")
}
} catch (error) {
console.log("[!] " + error)
setValidationMessage("An error occurred. Please contact us (see the sidebar)")
} finally {
setIsSubmitting(false)
}
}
function validateEmail(username: string, domain: string) {
return username.length > 0 && domain.length > 0 && validator.isEmail(`${username}@${domain}`)
}
function validatePassword(password: string) {
const passwordSchema = new PasswordValidator()
passwordSchema.is().min(8).is().max(128).has().letters().has().digits().has().not().spaces()
return passwordSchema.validate(password)
}
return (
<div className="flex h-screen items-center justify-center p-4">
<Card className="w-full max-w-md overflow-hidden">
<CardHeader>
<CardTitle>Account Setup</CardTitle>
<CardDescription>Create a new account or migrate an existing one.</CardDescription>
</CardHeader>
<CardContent>
<AnimatePresence mode="wait">
{formType === "initial" && (
<motion.div key="initial" {...fadeInOut} className="space-y-4">
<Button onClick={() => setFormType("create")} className="w-full h-16 text-lg">
<UserPlus className="mr-2 h-6 w-6" />
Create New Account
</Button>
<Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg">
<UserCog className="mr-2 h-6 w-6" />
Migrate p0ntus mail Account
</Button>
</motion.div>
)}
{formType === "create" && (
<motion.form key="create" {...fadeInOut} className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
name="name"
placeholder="Enter your name"
required
value={formData.name}
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="flex items-center space-x-2">
<Input
id="emailUsername"
name="emailUsername"
type="text"
placeholder="username"
required
value={formData.emailUsername}
onChange={handleInputChange}
className="flex-grow"
/>
<span className="text-muted-foreground">@</span>
<Select
name="emailDomain"
value={formData.emailDomain}
onValueChange={(value) => setFormData((prev) => ({ ...prev, emailDomain: value }))}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select domain" />
</SelectTrigger>
<SelectContent>
<SelectItem value="librecloud.cc">librecloud.cc</SelectItem>
<SelectItem value="pontusmail.org">pontusmail.org</SelectItem>
<SelectItem value="p0ntus.com">p0ntus.com</SelectItem>
<SelectItem value="ihate.college">ihate.college</SelectItem>
<SelectItem value="pontus.pics">pontus.pics</SelectItem>
<SelectItem value="dontbeevil.lol">dontbeevil.lol</SelectItem>
<SelectItem value="dont-be-evil.lol">dont-be-evil.lol</SelectItem>
<SelectItem value="strongintegrity.life">strongintegrity.life</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
placeholder="Enter your desired password"
required
value={formData.password}
onChange={handleInputChange}
/>
<p className="text-xs text-muted-foreground">
Password must be 8-64 characters long, include letters and digits, and not contain spaces.
</p>
</div>
<div className="flex items-center space-x-4 py-2">
<Switch
id="terms"
name="terms"
required
checked={formData.terms}
onCheckedChange={(checked) => setFormData((prev) => ({ ...prev, terms: checked }))}
/>
<Label htmlFor="terms" className="text-sm">
I agree to the{" "}
<Link href="/terms" className="underline">
Terms of Service
</Link>{" "}
and{" "}
<Link href="/privacy" className="underline">
Privacy Policy
</Link>
</Label>
</div>
</motion.form>
)}
{formType === "migrate" && (
<motion.form key="migrate" {...fadeInOut} className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="migrateName">Name</Label>
<Input
id="migrateName"
name="migrateName"
placeholder="Enter your name"
required
value={formData.migrateName}
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="flex items-center space-x-2">
<Input
id="emailUsername"
name="emailUsername"
type="text"
placeholder="username"
required
value={formData.emailUsername}
onChange={handleInputChange}
className="flex-grow"
/>
<span className="text-muted-foreground">@</span>
<Select
name="emailDomain"
value={formData.emailDomain}
onValueChange={(value) => setFormData((prev) => ({ ...prev, emailDomain: value }))}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select domain" />
</SelectTrigger>
<SelectContent>
<SelectItem value="librecloud.cc">librecloud.cc</SelectItem>
<SelectItem value="pontusmail.org">pontusmail.org</SelectItem>
<SelectItem value="p0ntus.com">p0ntus.com</SelectItem>
<SelectItem value="ihate.college">ihate.college</SelectItem>
<SelectItem value="pontus.pics">pontus.pics</SelectItem>
<SelectItem value="dontbeevil.lol">dontbeevil.lol</SelectItem>
<SelectItem value="dont-be-evil.lol">dont-be-evil.lol</SelectItem>
<SelectItem value="strongintegrity.life">strongintegrity.life</SelectItem>
</SelectContent>
</Select>
</div>
<p className="text-xs text-muted-foreground">
A username for Authentik will be generated based on your email. Contact support if a username isn&apos;t available.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="migratePassword">New Password</Label>
<Input
id="migratePassword"
name="migratePassword"
type="password"
placeholder="Enter your new password"
required
value={formData.migratePassword}
onChange={handleInputChange}
/>
<p className="text-xs text-muted-foreground">
Password must be 8-64 characters long, include letters and digits, and not contain spaces.
</p>
</div>
<div className="flex items-center space-x-4 py-2">
<Switch
id="migrateTerms"
name="migrateTerms"
required
checked={formData.migrateTerms}
onCheckedChange={(checked) => setFormData((prev) => ({ ...prev, migrateTerms: checked }))}
/>
<Label htmlFor="migrateTerms" className="text-sm">
I agree to the{" "}
<Link href="/terms" className="underline">
Terms of Service
</Link>{" "}
and{" "}
<Link href="/privacy" className="underline">
Privacy Policy
</Link>
</Label>
</div>
</motion.form>
)}
</AnimatePresence>
</CardContent>
<CardFooter>
<AnimatePresence mode="wait">
{formType !== "initial" ? (
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
<Button
type="submit"
className="w-full mb-4"
disabled={!isValid || isSubmitting}
onClick={handleSubmit}
>
{isSubmitting ? (
<motion.div
className="h-5 w-5 animate-spin rounded-full border-b-2 border-white"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Number.POSITIVE_INFINITY, ease: "linear" }}
/>
) : (
getButtonIcon()
)}
{isSubmitting ? "Submitting..." : validationMessage}
</Button>
<Button variant="outline" className="w-full" onClick={() => setFormType("initial")}>
Back
</Button>
</motion.div>
) : (
<motion.div key="welcome" {...fadeInOut} className="flex w-full justify-center items-center">
<span className="text-sm text-center">Welcome to the LibreCloud family!</span>
<Heart className="h-4 w-4 ml-1" />
</motion.div>
)}
</AnimatePresence>
</CardFooter>
</Card>
</div>
)
}

View File

@ -0,0 +1,160 @@
"use client";
import { motion } from "framer-motion"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { ExternalLink, Mail, Sparkles, Lock, BadgeCheck, CircleArrowRight } from "lucide-react"
import { SiGitea, SiAuthentik } from "react-icons/si"
import Link from "next/link"
import { Separator } from "@/components/ui/separator"
import { useTheme } from "next-themes"
const services = [
{
title: "Email",
description: "You now have access to send and receive emails with the email you created at signup.",
link: "https://mail.librecloud.cc/",
icon: Mail,
},
{
title: "Pass",
description: "Securely store and manage your passwords across devices with Vaultwarden.",
link: "https://vaultwarden.p0ntus.com/",
icon: Lock,
},
{
title: "Git",
description: "Host your repositories and run actions free of charge on our Gitea instance.",
link: "https://git.pontusmail.org/",
icon: SiGitea,
},
{
title: "Authentik",
description: "A secure single-sign-on service for easy login to your other services.",
link: "https://git.pontusmail.org/",
icon: SiAuthentik,
},
]
const WelcomePage = () => {
const { resolvedTheme } = useTheme()
return (
<div className="flex flex-col md:flex-row h-screen overflow-hidden">
<div className="w-full h-1/5 sm:h-2/5 md:h-full md:w-2/5 flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-black/30 via-black/20 to-transparent dark:from-white/30 dark:via-white/20 dark:to-transparent"></div>
<div
style={{
backgroundImage: resolvedTheme === "dark"
? "url(/noise-dark.png)"
: "url(/noise-light.png)",
opacity: 0.1,
}}
className="absolute inset-0"
/>
<div className="relative z-10 p-8 md:pl-12">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-4xl md:text-5xl font-bold text-gray-800 dark:text-white text-center md:text-left"
>
Welcome to LibreCloud
</motion.h1>
</div>
</div>
<div className="w-full md:w-3/5 lg:w-4/5 p-4 md:p-8 h-full overflow-y-auto">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.8 }}>
<BadgeCheck className="w-16 h-16 text-primary mx-auto" />
<h2 className="text-2xl md:text-3xl font-semibold my-6 text-center">Your account was created</h2>
<Separator className="my-8" />
<h2 className="text-2xl md:text-3xl font-semibold my-6">You&apos;ve unlocked</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{services.map((service, index) => (
<Card key={index}>
<CardHeader>
<CardTitle className="flex items-center">
<service.icon className="w-5 h-5 mr-2" />
{service.title}
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{service.description}</CardDescription>
<Button variant="link" className="mt-2 p-0" asChild>
<Link href={service.link}>
Explore <ExternalLink className="w-4 h-4 ml-1" />
</Link>
</Button>
</CardContent>
</Card>
))}
</div>
<Separator className="my-8" />
<h2 className="text-2xl md:text-3xl font-semibold my-6">Pay less, do more</h2>
<div className="space-y-6">
<motion.div
className="bg-card p-6 rounded-lg shadow-md"
whileHover={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 300 }}
>
<h3 className="text-xl font-semibold flex items-center mb-3">
<Sparkles className="w-6 h-6 mr-2" />
Did You Know?
</h3>
<p>
LibreCloud makes <b>ZERO</b> profit off upgrades, while doing the heavy lifting in the background to ensure a connected experience.
</p>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<motion.div
className="bg-primary/10 p-6 rounded-lg shadow-md"
whileHover={{ y: -5 }}
transition={{ type: "spring", stiffness: 300 }}
>
<h4 className="text-lg font-semibold mb-2">Generative AI</h4>
<p className="text-sm">
Use GenAI tools at API pricing, without compromising on the UI.
</p>
</motion.div>
<motion.div
className="bg-primary/10 p-6 rounded-lg shadow-md"
whileHover={{ y: -5 }}
transition={{ type: "spring", stiffness: 300 }}
>
<h4 className="text-lg font-semibold mb-2">Account Upgrades</h4>
<p className="text-sm">
Add additional storage or services from our host at no additional fee from LibreCloud.
</p>
</motion.div>
</div>
</div>
<Separator className="my-8" />
<h2 className="text-2xl md:text-3xl font-semibold my-6">Now, we set you free</h2>
<p>
Now, it&apos;s your time to explore. All we ask is you use your services in a fair way. Account bans will be enforced for abuse of our services for spamming, scamming, and all that nasty stuff.
</p>
<p className="mt-4">
We live in the &quot;Support&quot; tab on your dashboard, and on Telegram if you need any help. Please be civil and patient, and we&apos;ll do our best to help you out.
</p>
<p className="mt-4">
From here, you can proceed to sign in to your newly created account with Authentik. It will handle all the sign-ins for your account except for Pass (Vaultwarden).
</p>
<Link href="/account/login">
<Button className="mt-8"><CircleArrowRight /> Login</Button>
</Link>
</motion.div>
</div>
</div>
)
}
export default WelcomePage

View File

@ -0,0 +1,2 @@
import { handlers } from "@/auth"
export const { GET, POST } = handlers

46
app/api/git/user/route.ts Normal file
View File

@ -0,0 +1,46 @@
import { auth } from "@/auth"
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
export async function GET() {
try {
const session = await auth()
if (!session || !session.user?.email) {
return NextResponse.json({ error: "Unauthorized - Please login first" }, { status: 401 })
}
const { email } = session.user
const dbUser = await prisma.user.findUnique({
where: { email },
})
if (!dbUser) {
return NextResponse.json({ message: "User not found in database" })
}
const response = await fetch(`https://git.pontusmail.org/api/v1/users/${dbUser.username}`, {
headers: {
Authorization: `Bearer ${process.env.GITEA_API_KEY}`,
"Content-Type": "application/json",
},// don't forget to smile today :)
})
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch Git user data" }, { status: response.status })
}
const userData = await response.json()
if (userData.email !== email) {
return NextResponse.json({ error: "Email verification failed" }, { status: 403 })
}
return NextResponse.json(userData)
} catch (error) {
console.error("Git status API error:", error)
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
}
}

View File

@ -0,0 +1,125 @@
import { prisma } from "@/lib/prisma"
import axios from "axios"
import { NextResponse } from "next/server"
// This endpoint has three functions:
// (1) Create a new LibreCloud user (Authentik, Email)
// (2) Link a user with their Git account via DB (creates user in db)
// (3) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email)
async function createEmail(email: string, password: string, migrate: boolean) {
const response = await axios.post("http://localhost:6723/accounts/add", {
email,
password,
migrate,
})
const responseData = response.data
if (responseData.success) {
return response.data
} else if (responseData.error) {
return { success: false, message: responseData.error }
} else {
return { success: false, message: "Failed to create email account" }
}
}
export async function POST(request: Request) {
try {
const body = await request.json()
const { name, username, email, password, migrate } = body
console.log(body)
if (!name || !email || !password) {
return NextResponse.json({ success: false, message: "Request is incomplete" })
}
// Insert into database if user provided Git username
if (username) {
const user = await prisma.user.create({
data: {
email,
username,
},
})
return NextResponse.json(user)
} else {
// Create Authentik user
const genUser = email.split("@")[0]
const userData = {
username: genUser,
name,
is_active: true,
groups: [
"b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik
],
email,
type: "internal",
}
console.log(userData)
const response = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: "https://auth.librecloud.cc/api/v3/core/users/",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: JSON.stringify(userData),
validateStatus: () => true, // capture response even for error status codes
})
if (response.data?.detail) {
console.log(response.data.detail)
}
if (response.status !== 201) {
console.log(response.data)
if (response.data.username && response.data.username[0] === "This field must be unique.") {
return NextResponse.json({ success: false, message: "Username already exists" })
}
return NextResponse.json({ success: false, message: "Failed to create user in Authentik" })
} else {
const userID = response.data.pk
const updData = {
password,
}
const updCfg = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: `https://auth.librecloud.cc/api/v3/core/users/${userID}/set_password/`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: updData,
validateStatus: () => true, // capture response even for error status codes
})
if (updCfg.data?.detail) {
console.log(updCfg.data.detail)
}
if (updCfg.status === 204) {
// account created successfully, now create email
const emailRes = await createEmail(email, password, migrate)
return NextResponse.json(emailRes)
} else if (updCfg.status === 400) {
return NextResponse.json({ success: false, message: "Bad Request - Failed to set Authentik password" })
} else {
return NextResponse.json({ success: false, message: "Internal Server Error - Failed to set Authentik password" })
}
}
}
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.data?.detail) {
console.log("Axios error detail:", error.response.data.detail)
} else {
console.log(error)
}
return NextResponse.json({ success: false, message: "Internal server error - Failed to create user" })
}
}

View File

@ -1,31 +0,0 @@
import type { LucideIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
interface FeatureCardProps {
title: string
description: string
link: string
icon: LucideIcon
}
const FeatureCard = ({ title, description, link, icon: Icon }: FeatureCardProps) => {
return (
<div className="bg-gray-800 rounded-lg p-6 shadow-lg">
<div className="flex items-center justify-center w-12 h-12 rounded-md bg-blue-500 text-white mb-4">
<Icon className="h-6 w-6" />
</div>
<h3 className="text-lg font-medium text-white">{title}</h3>
<p className="mt-2 text-base text-gray-300">{description}</p>
<a href={link}>
<Button
className="mt-4 w-full text-white bg-blue-600 hover:bg-blue-700"
>
Sign Up
</Button>
</a>
</div>
)
}
export default FeatureCard

View File

@ -1,87 +0,0 @@
'use client';
import { useState } from "react"
import Link from "next/link"
import { Menu, X, Server, Home, User } from "lucide-react"
const Navbar = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<nav className="bg-gray-800 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="relative flex items-center justify-between h-16">
<div className="flex-shrink-0 flex items-center">
<Link href="/" className="flex items-center">
<span className="text-white text-xl font-bold">LibreCloud</span>
</Link>
</div>
<div className="hidden md:block">
<div className="ml-10 flex items-center space-x-4">
<Link
href="/"
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
>
<Home className="mr-2 h-5 w-5" /> Home
</Link>
<Link
href="/#services"
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
>
<Server className="mr-2 h-5 w-5" /> Services
</Link>
<Link
href="/account/login"
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
>
<User className="mr-2 h-5 w-5" /> My Account
</Link>
</div>
</div>
<div className="md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
aria-expanded={isOpen}
>
<span className="sr-only">Open main menu</span>
{isOpen ? (
<X className="block h-6 w-6" aria-hidden="true" />
) : (
<Menu className="block h-6 w-6" aria-hidden="true" />
)}
</button>
</div>
</div>
</div>
{isOpen && (
<div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<Link
href="#"
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
<Home className="mr-2 h-5 w-5" /> Home
</Link>
<Link
href="#services"
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
<Server className="mr-2 h-5 w-5" /> Services
</Link>
<Link
href="/account/login"
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
<User className="mr-2 h-5 w-5" /> My Account
</Link>
</div>
</div>
)}
</nav>
)
}
export default Navbar

View File

@ -1,38 +0,0 @@
import Link from "next/link"
import { Cog, LogOut, Gauge } from "lucide-react"
import { Flex, Card } from "@radix-ui/themes"
import { usePathname } from "next/navigation"
const Sidebar = () => {
const pathname = usePathname()
const navItems = [
{ href: "/account/dashboard", icon: Gauge, label: "Dashboard" },
{ href: "/account/settings", icon: Cog, label: "Settings" },
{ href: "/account/logout", icon: LogOut, label: "Logout" },
]
return (
<Card className="w-64 h-screen bg-gray-800 p-4">
<Flex direction="column" gap="2">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={`
flex items-center px-4 py-2 rounded-md text-white
transition-colors duration-200
${pathname === item.href ? "bg-blue-600 hover:bg-blue-700" : "hover:bg-gray-700"}
`}
>
<item.icon className="w-5 h-5 mr-3" />
<span className="text-sm font-medium">{item.label}</span>
</Link>
))}
</Flex>
</Card>
)
}
export default Sidebar

View File

@ -2,78 +2,100 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 84% 4.9%; --foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%; --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%; --popover: 0 0% 100%;
--destructive: 0 84.2% 60.2%; --popover-foreground: 222.2 84% 4.9%;
--destructive-foreground: 210 40% 98%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%; --primary: 222.2 47.4% 11.2%;
--chart-2: 173 58% 39%; --primary-foreground: 210 40% 98%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --secondary: 210 40% 96.1%;
--chart-5: 27 87% 67%; --secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem; --radius: 0.5rem;
--sidebar-background: 0 0% 98%; --sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%; --sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%; --sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%; --sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%; --sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%; --sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 222.2 84% 4.9%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%; --muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%;
--destructive: 0 62.8% 30.6%; --popover-foreground: 210 40% 98%;
--destructive-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%; --primary: 210 40% 98%;
--chart-2: 160 60% 45%; --primary-foreground: 222.2 47.4% 11.2%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --secondary: 217.2 32.6% 17.5%;
--chart-5: 340 75% 55%; --secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 217.2 32.6% 17.5%;
--sidebar-background: 240 5.9% 10%; --sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%; --sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%; --sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%; --sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%; --sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
} }
} }
@ -85,4 +107,5 @@ body {
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View File

@ -1,11 +1,8 @@
import "./globals.css" import "./globals.css"
import type { Metadata } from "next" import type { Metadata } from "next"
import Head from "next/head" import { Providers } from "@/app/providers"
import { Inter } from "next/font/google" import type React from "react"
import Navbar from "../app/components/Navbar" import { GeistSans } from 'geist/font/sans';
import "@radix-ui/themes/styles.css";
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = { export const metadata: Metadata = {
title: "LibreCloud", title: "LibreCloud",
@ -13,25 +10,15 @@ export const metadata: Metadata = {
} }
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<html lang="en"> <html lang="en" className={GeistSans.className} suppressHydrationWarning>
<Head> <body>
<title>{`${metadata.title}`}</title> <Providers>{children}</Providers>
<meta name="description" content={metadata.description ?? ''} /> </body>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
</Head>
<body className={`${inter.className} bg-gray-900 text-gray-100`}>
<Navbar />
{children}
</body>
</html> </html>
) )
} }

View File

@ -1,12 +1,14 @@
import Hero from "../app/components/Hero" import Hero from "@/components/pages/main/Hero"
import FeatureCard from "../app/components/FeatureCard" import FeatureCard from "@/components/pages/main/FeatureCard"
import { Mail, Lock, Code, } from "lucide-react" import { Mail, Lock, Disc3 } from "lucide-react"
import { SiGitea, SiAuthentik } from "react-icons/si";
import Navbar from "@/components/pages/main/Navbar"
export default function Home() { export default function Home() {
const features = [ const features = [
{ {
title: "Email", title: "Email",
description: "4GB of free email storage and a synced calendar.", description: "Free email service with webmail and antispam, powered by a custom docker-mailserver setup.",
link: "https://pontusmail.org/", link: "https://pontusmail.org/",
icon: Mail, icon: Mail,
}, },
@ -17,27 +19,42 @@ export default function Home() {
icon: Lock, icon: Lock,
}, },
{ {
title: "Repo Hosting", title: "Git",
description: "Host your code repositories on our Gitea instance.", description: "Host your repositories and run actions free of charge on our Gitea instance.",
link: "https://git.pontusmail.org/", link: "https://git.pontusmail.org/",
icon: Code, icon: SiGitea,
},
{
title: "Authentik",
description: "A secure single-sign-on service for easy login to your other services.",
link: "https://git.pontusmail.org/",
icon: SiAuthentik,
},
{
title: "Music",
description: "Coming soon. Host your music on our community server and stream it everywhere",
link: "https://git.pontusmail.org/",
icon: Disc3,
}, },
] ]
return ( return (
<main className="min-h-screen"> <div className="min-h-screen bg-gradient-to-b from-gray-950 to-gray-900">
<Hero /> <Navbar />
<section id="services" className="py-20 bg-gray-900"> <main>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <Hero />
<h2 className="text-4xl font-extrabold text-white text-center mb-12">Services</h2> <section id="services" className="py-20">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{features.map((feature, index) => ( <h2 className="text-4xl font-extrabold text-center mb-12 text-white">Our Services</h2>
<FeatureCard key={index} {...feature} /> <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
))} {features.map((feature, index) => (
<FeatureCard key={index} {...feature} />
))}
</div>
</div> </div>
</div> </section>
</section> </main>
</main> </div>
) )
} }

13
app/providers.tsx Normal file
View File

@ -0,0 +1,13 @@
"use client"
import { ThemeProvider } from "next-themes"
import type { ReactNode } from "react"
export function Providers({ children }: { children: ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}

14
auth.ts Normal file
View File

@ -0,0 +1,14 @@
import NextAuth from "next-auth"
import Authentik from "next-auth/providers/authentik"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Authentik],
callbacks: {
authorized: async ({ auth }) => {
return !!auth
},
},
pages: {
signIn: "/account/login",
},
})

BIN
bun.lockb

Binary file not shown.

View File

@ -0,0 +1,32 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { HardDrive } from "lucide-react";
import React, { useState } from "react";
export const DiskUsage = () => {
// TODO: Implement disk usage card logic
//const [diskUsage, setDiskUsage] = useState(0);
//const [quotaGB, setQuotaGB] = useState(0);
//const [quotaUsedGB, setQuotaUsedGB] = useState(0);
const [diskUsage] = useState(0);
const [quotaGB] = useState(0);
const [quotaUsedGB] = useState(0);
return (
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Disk Usage</CardTitle>
<CardDescription>Your email accounts&apos; disk quota usage</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4">
<HardDrive className="h-8 w-8 text-primary" />
<div className="flex-1">
<Progress value={diskUsage} className="mb-2" />
<p className="text-sm text-muted-foreground">{quotaUsedGB}GB of {quotaGB}GB used</p>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,85 @@
import { useState, useEffect } from "react";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
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);
useEffect(() => {
const fetchGitStatus = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/git/user")
const data = await response.json()
if (!response.ok) {
if (data.error) {
throw new Error(data.error)
} else {
throw new Error(`HTTP error: ${response.status}`)
}
}
if (data.is_admin) {
setIsAdmin(true)
}
if (!data.message) {
setGitStatus(true)
}
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message)
}
} finally {
setIsLoading(false)
}
};
fetchGitStatus()
}, []);
return (
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Linked Accounts</CardTitle>
<CardDescription>LibreCloud-connected services</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2">
<li className="flex items-center">
{isLoading ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<span
className={`w-2 h-2 rounded-full mr-2 ${
gitStatus ? "bg-green-500" : "bg-red-500"
}`}
></span>
)}
{isAdmin ? (
<div>
<span>LibreCloud Git</span>
<Badge
className="ml-2"
>
Admin
</Badge>
</div>
) : (
<span>LibreCloud Git</span>
)}
</li>
<li className="flex items-center">
<span className="w-2 h-2 rounded-full bg-green-500 mr-2"></span>
<span>p0ntus mail</span>
</li>
</ul>
<div className="mt-4">
{error && <p className="text-red-500">{error}</p>}
</div>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,45 @@
import React, { useState, useEffect } from "react";
import Cookies from "js-cookie";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Check } from "lucide-react";
export const WelcomeCard = () => {
const [visible, setVisible] = useState(true);
useEffect(() => {
const isRead = Cookies.get("welcome-read");
if (isRead === "true") {
setVisible(false);
}
}, []);
const handleMarkAsRead = () => {
Cookies.set("welcome-read", "true");
setVisible(false);
};
if (!visible) return null;
return (
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Welcome to your dashboard</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm">
Thanks for logging in! Here you can manage your account and the services available to you.
</p>
<p className="text-sm mt-4">
Were thrilled to have you on board, and if you need <i>anything</i>, dont hesitate to contact support (see the sidebar).
</p>
<p className="text-sm mt-4">Thats all, have a great day!</p>
</CardContent>
<CardFooter>
<Button className="w-full" onClick={handleMarkAsRead}>
<Check /> Mark as Read
</Button>
</CardFooter>
</Card>
);
};

View File

@ -0,0 +1,20 @@
"use client";
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
import { DoorOpen } from "lucide-react";
import { logout } from "@/actions/logout";
export function SidebarSignOut() {
return (
<SidebarMenuItem>
<form action={logout}>
<SidebarMenuButton type="submit">
<DoorOpen />
<span>Logout</span>
</SidebarMenuButton>
</form>
</SidebarMenuItem>
);
}
export default SidebarSignOut;

View File

@ -0,0 +1,25 @@
"use client";
import { useSidebar } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { Menu } from "lucide-react";
const SidebarToggle = () => {
// TODO: Sidebar logic needs fixing (hide sidebar on button click)
const { toggleSidebar } = useSidebar();
return (
<div className="fixed bottom-4 left-4 mb-10 ml-0.5 lg:ml-64">
<Button
size="icon"
variant="outline"
onClick={toggleSidebar}
>
<Menu className="h-[1.2rem] w-[1.2rem]" />
<span className="sr-only">Toggle sidebar</span>
</Button>
</div>
);
};
export default SidebarToggle;

View File

@ -0,0 +1,43 @@
"use client"
import * as React from "react"
import { Moon, Sun } 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 ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="outline"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,28 @@
"use client"
import {useEffect, useState} from "react"
export function Footer() {
const [renderTime, setRenderTime] = useState<number | null>(null)
useEffect(() => {
const startTime = performance.now()
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const endTime = performance.now()
const timeTaken = endTime - startTime
setRenderTime(timeTaken)
})
})
}, [])
return (
<footer className="py-2 px-4 text-sm text-muted-foreground bg-muted">
<div className="flex justify-between">
<p>Created by a community, not a company.</p>
{renderTime !== null ? <p>Page rendered in {renderTime.toFixed(2)} ms</p> : <p>Calculating render time...</p>}
</div>
</footer>
)
}

View File

@ -0,0 +1,197 @@
"use client"
//import { useState } from "react"
import { User, Users, Clock } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
//import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
//import { Input } from "@/components/ui/input"
//import { Label } from "@/components/ui/label"
//import { Switch } from "@/components/ui/switch"
//import { Textarea } from "@/components/ui/textarea"
interface DashboardState {
gitUser: string;
gitAvatar?: string;
gitLastLogin?: string;
gitFollowerCt: number;
gitFollowingCt: number;
gitIsAdmin: boolean;
gitEmail?: string;
}
export const GitTab = ({ dashboardState }: { dashboardState: DashboardState }) => {
/*
This is disabled for later, so I can finish working on it. I want to focus on essential services first.
const [newUsername, setNewUsername] = useState("")
const [newPassword, setNewPassword] = useState("")
const [newEmail, setNewEmail] = useState("")
const [repoName, setRepoName] = useState("")
const [isPrivate, setIsPrivate] = useState(false)
const [repoDescription, setRepoDescription] = useState("")
const [autoInit, setAutoInit] = useState(true)
const handleUsernameChange = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log("Data:", newUsername)
}
const handlePasswordChange = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log("Changing password")
}
const handleEmailChange = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log("Data:", newEmail)
}
const handleCreateRepo = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log("Data:", { repoName, isPrivate, repoDescription, autoInit })
}
*/
const convDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleString();
}
return (
<div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Profile</CardTitle>
<CardDescription>An overview of your LibreCloud Git account</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-4">
<Avatar className="w-20 h-20">
<AvatarImage src={dashboardState.gitAvatar || "/placeholder.svg"} />
<AvatarFallback>
<User className="w-10 h-10" />
</AvatarFallback>
</Avatar>
<div>
<h3 className="text-2xl font-semibold flex items-center">
{dashboardState.gitUser}
{dashboardState.gitIsAdmin && <Badge className="ml-2">Admin</Badge>}
</h3>
<div className="flex space-x-4 text-sm text-muted-foreground">
<span className="flex items-center">
<Users className="w-4 h-4 mr-1" /> {dashboardState.gitFollowerCt} followers
</span>
<span className="flex items-center">
<Users className="w-4 h-4 mr-1" /> {dashboardState.gitFollowingCt} following
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4" />
<span>Last login: {dashboardState.gitLastLogin === "Never" ? "Never" : (dashboardState.gitLastLogin && convDate(dashboardState.gitLastLogin)) || "N/A"}</span>
</div>
</CardContent>
</Card>
</div>
{/*
This is disabled for later, so I can finish working on it. I want to focus on essential services first.
<div className="grid gap-6 mt-6 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Change Username</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleUsernameChange} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current-username">Current Username</Label>
<Input id="current-username" value={dashboardState.gitUser} disabled />
</div>
<div className="space-y-2">
<Label htmlFor="new-username">New Username</Label>
<Input id="new-username" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} />
</div>
<Button type="submit">Change Username</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Change Password</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<Input
id="new-password"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<Button type="submit">Change Password</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Change Email</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleEmailChange} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current-email">Current Email</Label>
<Input
id="current-email"
value={dashboardState.gitEmail?.replace(/(.{2})(.*)(?=@)/, (_, a, b) => a + "*".repeat(b.length)) || ""}
disabled
/>
</div>
<div className="space-y-2">
<Label htmlFor="new-email">New Email</Label>
<Input id="new-email" type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
</div>
<Button type="submit">Change Email</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Create New Repository</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateRepo} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="repo-name">Repository Name</Label>
<Input id="repo-name" value={repoName} onChange={(e) => setRepoName(e.target.value)} />
</div>
<div className="flex items-center space-x-2">
<Switch id="repo-private" checked={isPrivate} onCheckedChange={setIsPrivate} />
<Label htmlFor="repo-private">Private Repository</Label>
</div>
<div className="space-y-2">
<Label htmlFor="repo-description">Description</Label>
<Textarea
id="repo-description"
value={repoDescription}
onChange={(e) => setRepoDescription(e.target.value)}
/>
</div>
<div className="flex items-center space-x-2">
<Switch id="repo-autoinit" checked={autoInit} onCheckedChange={setAutoInit} />
<Label htmlFor="repo-autoinit">Initialize with README</Label>
</div>
<Button type="submit">Create Repository</Button>
</form>
</CardContent>
</Card>
</div>*/}
</div>
)
}

View File

@ -0,0 +1,104 @@
import { User, Bell } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Separator } from "@/components/ui/separator"
import { auth } from "@/auth"
import { ThemeToggle } from "@/components/custom/ThemeToggle"
export const Header = async () => {
const session = await auth()
if (!session?.user) return null
const notifications = [
{
id: 1,
title: "Coming soon!",
description: "Notification support will be added shortly. Thanks for checking me out!",
time: "now"
}
]
return (
<header className="sticky top-0 h-16 z-20 bg-background border-b p-4 flex items-center justify-between">
<div className="flex items-center space-x-4">
<Avatar>
{session.user.image || isNaN(Number(session.user.image)) ? (
<AvatarFallback>
<User/>
</AvatarFallback>
) : (
<div>
<AvatarImage src={session.user.image || "https://i.pravatar.cc/"}/>
<AvatarFallback>
<User/>
</AvatarFallback>
</div>
)}
</Avatar>
<div className="hidden sm:block">
<h2 className="font-semibold">{session.user.name}</h2>
<p className="text-sm text-muted-foreground">LibreCloud User</p>
</div>
</div>
<div className="flex items-center space-x-2">
<ThemeToggle />
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
>
<Bell className="h-5 w-5"/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0 m-4 mt-0">
<div className="p-4">
<div className="flex items-center justify-between">
<h4 className="font-semibold">Notifications</h4>
<Button variant="ghost" size="sm" className="text-xs">
Mark all as read
</Button>
</div>
</div>
<Separator/>
<div className="max-h-80 overflow-y-auto">
{notifications.length > 0 ? (
notifications.map((notification) => (
<div
key={notification.id}
className="p-4 hover:bg-muted transition-colors cursor-pointer"
>
<div className="flex justify-between items-start mb-1">
<h5 className="font-medium">{notification.title}</h5>
<span className="text-xs text-muted-foreground">
{notification.time}
</span>
</div>
<p className="text-sm text-muted-foreground">
{notification.description}
</p>
</div>
))
) : (
<div className="p-4 text-center text-muted-foreground">
No new notifications
</div>
)}
</div>
<Separator/>
<div className="p-2">
<Button variant="ghost" className="w-full text-sm">
View all notifications
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</header>
)
}

View File

@ -0,0 +1,12 @@
//import { DiskUsage } from "@/components/cards/dashboard/DiskUsage"
import { WelcomeCard } from "@/components/cards/dashboard/overview/WelcomeCard"
import { LinkedAccounts } from "@/components/cards/dashboard/overview/LinkedAccounts"
export const OverviewTab = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<WelcomeCard />
{/* TODO: Disabled for later - <DiskUsage />*/}
<LinkedAccounts />
</div>
)

View File

@ -0,0 +1,38 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ShieldCheck } from "lucide-react";
export const SecurityTab = () => {
return (
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
<Card>
{/* TODO: Implement security checks */}
<CardHeader>
<CardTitle>Security Check</CardTitle>
<CardDescription>Evaluate the security of your account with a simple check!</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm mb-6">Automatic security scans will be arriving shortly!</p>
<Button className="w-full" disabled>
<ShieldCheck className="h-4 w-4" /> Run Security Scan
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Security Recommendations</CardTitle>
<CardDescription>Steps you can take to improve your account&apos;s security</CardDescription>
</CardHeader>
<CardContent>
<ul className="list-disc pl-5 space-y-2">
<li>Enable Two-Factor Authentication</li>
<li>Use a strong and unique password</li>
<li>Run security checks often (just in case)</li>
<li>Always double-check the URL (librecloud.cc only!)</li>
</ul>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,83 @@
import Link from "next/link"
import { Mail, Key, ExternalLink } from "lucide-react"
import { SiGitea, SiAuthentik } from "react-icons/si";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export const ServicesTab = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{/* TODO: move to component */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Mail className="mr-2 h-4 w-4" />
Webmail
</CardTitle>
<CardDescription className="pt-4">Send, read, and manage your email account from a web browser!</CardDescription>
</CardHeader>
<CardContent>
<Button>
<ExternalLink className="h-4 w-4" />
<Link href="https://mail.librecloud.cc/">
Open App
</Link>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<SiGitea className="mr-2 h-4 w-4" />
Git
</CardTitle>
<CardDescription className="pt-4">Host your repositories and run Actions on a fair usage policy.</CardDescription>
</CardHeader>
<CardContent>
<Button>
<ExternalLink className="h-4 w-4" />
<Link href="https://git.pontusmail.org/">
Open App
</Link>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Key className="mr-2 h-4 w-4" />
Pass
</CardTitle>
<CardDescription className="pt-4">Securely store your passwords, notes, and 2FA codes with Vaultwarden.</CardDescription>
</CardHeader>
<CardContent>
<Button>
<ExternalLink className="h-4 w-4" />
<Link href="https://vaultwarden.p0ntus.com/">
Open App
</Link>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<SiAuthentik className="mr-2 h-4 w-4" />
Authentik
</CardTitle>
<CardDescription className="pt-4">Manage your single-sign-on account for all LibreCloud services.</CardDescription>
</CardHeader>
<CardContent>
<Button>
<ExternalLink className="h-4 w-4" />
<Link href="https://auth.librecloud.cc/">
Open App
</Link>
</Button>
</CardContent>
</Card>
</div>
)

View File

@ -0,0 +1,116 @@
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"
import LogoutMenuItem from "@/components/custom/LogoutMenuItem"
import React from "react"
import Link from "next/link";
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,
},
]
const toolsGroupSidebarItems = [
{
title: "Exchange Crypto",
url: "/account/dashboard/exchange",
icon: Bitcoin,
},
{
title: "Statistics",
url: "/account/dashboard/statistics",
icon: ChartSpline,
},
]
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}>
<SidebarMenuButton asChild>
<Link href={item.url}>
<item.icon />
<span>{item.title}</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>
)

View File

@ -0,0 +1,185 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link";
import {ArrowDownToLine, BadgeDollarSign, HeartHandshake} from "lucide-react";
export const EmailTab = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Linux Email Clients</CardTitle>
<CardDescription>Applications which will allow you to view, send, and manage your email which are supported on Linux.</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://www.thunderbird.net/en-US/thunderbird/all/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Thunderbird</span>
<span className="ml-4 text-sm text-gray-600">by Mozilla</span>
</li>
<li className="mb-2">
<Link href="https://wiki.gnome.org/Apps/Evolution" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Evolution</span>
<span className="ml-4 text-sm text-gray-600">by GNOME</span>
</li>
<li className="mb-2">
<Link href="https://wiki.gnome.org/Apps/Geary" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Geary</span>
<span className="ml-4 text-sm text-gray-600">by GNOME</span>
</li>
<li className="mb-2">
<Link href="https://www.claws-mail.org/downloads.php" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Claws Mail</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Windows Email Clients</CardTitle>
<CardDescription>Applications which will allow you to view, send, and manage your email which are supported on Windows.</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://www.thunderbird.net/en-US/thunderbird/all/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Thunderbird</span>
<span className="ml-4 text-sm text-gray-600">by Mozilla</span>
</li>
<li className="mb-2">
<Link href="https://www.microsoft.com/en-us/microsoft-365/outlook/outlook-for-windows" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">Outlook</span>
<span className="ml-4 text-sm text-gray-600">by Microsoft</span>
</li>
<li className="mb-2">
<Link href="https://www.claws-mail.org/win32/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Claws Mail</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Android Email Clients</CardTitle>
<CardDescription>Applications which will allow you to view, send, and manage your email which are supported on Android.</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://www.thunderbird.net/en-US/mobile/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Thunderbird</span>
<span className="ml-4 text-sm text-gray-600">by Mozilla</span>
</li>
<li className="mb-2">
<Link href="https://k9mail.app/download" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">K-9 Mail</span>
<span className="ml-4 text-sm text-gray-600">by Mozilla</span>
</li>
<li className="mb-2">
<Link href="https://email.faircode.eu/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">FairMail</span>
</li>
<li className="mb-2">
<Link href="https://www.aqua-mail.com/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">Aqua Mail</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>iOS Email Clients</CardTitle>
<CardDescription>Applications which will allow you to view, send, and manage your email which are supported on iOS.</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/mail/id1108187098/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<span className="font-semibold">Apple Mail</span>
<span className="ml-4 text-sm text-gray-600">by Apple</span>
</li>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/microsoft-outlook/id951937596" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">Outlook</span>
<span className="ml-4 text-sm text-gray-600">by Microsoft</span>
</li>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/spark-mail-ai-email-inbox/id997102246" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">Spark Mail</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>macOS Email Clients</CardTitle>
<CardDescription>Applications which will allow you to view, send, and manage your email which are supported on macOS.</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1 text-red-400" />
<span className="font-semibold">Apple Mail</span>
<span className="ml-4 text-sm text-gray-600">by Apple</span>
</li>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/microsoft-outlook/id985367838" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">Outlook</span>
<span className="ml-4 text-sm text-gray-600">by Microsoft</span>
</li>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/spark-mail-ai-email-inbox/id6445813049" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">Spark Mail</span>
</li>
</ul>
</CardContent>
</Card>
</div>
)

View File

@ -0,0 +1,64 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link";
import {ArrowDownToLine, BadgeDollarSign, HeartHandshake} from "lucide-react";
export const GitTab = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Mobile Clients</CardTitle>
<CardDescription>Manage your Git account from your mobile device!</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://gitnex.com/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">GitNex</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Desktop Clients</CardTitle>
<CardDescription>Use the full suite of Git tools from the command line!</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://git-scm.com/downloads/linux" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Git for Linux (CLI)</span>
</li>
<li className="mb-2">
<Link href="https://git-scm.com/downloads/win" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Git for Windows (CLI)</span>
</li>
<li className="mb-2">
<Link href="https://git-scm.com/downloads/mac" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Git for Mac (CLI)</span>
</li>
<li className="mb-2">
<Link href="https://www.gitkraken.com/git-client" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span className="font-semibold">GitKraken Desktop (GUI)</span>
</li>
</ul>
</CardContent>
</Card>
</div>
)

View File

@ -0,0 +1,62 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowDownToLine, BadgeDollarSign, HeartHandshake } from "lucide-react"
import { TfiLinux, TfiMicrosoftAlt, TfiAndroid, TfiApple } from "react-icons/tfi";
export const HomeTab = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Welcome</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm">
In order to make it easy to access and use our services, we have compiled resources which may help you with
using LibreCloud services.
</p>
<p className="text-sm mt-4">
Click any of the tabs to be taken to a category of your choosing. We have resources for:
</p>
<div className="flex justify-center items-center gap-4 text-muted-foreground my-6">
<TfiLinux size={24} />
<TfiMicrosoftAlt size={24} />
<TfiAndroid size={24} />
<TfiApple size={24} />
</div>
<p className="text-sm">
All downloads are <i>not</i> hosted by LibreCloud and are from their original source only.
</p>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Icon Key</CardTitle>
<CardDescription>While browsing downloads, you might want to know what each icon means.</CardDescription>
</CardHeader>
<CardContent>
<div>
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
<span>
Download <span className="text-xs text-muted-foreground ml-2">(click to download)</span>
</span>
</div>
<div className="mt-2">
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span>Open Source</span>
</div>
<div className="mt-2">
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-green-400" />
<span>Completely free</span>
</div>
<div className="mt-2">
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-orange-400" />
<span>Offers free tier (restricted)</span>
</div>
<div className="mt-2">
<BadgeDollarSign className="inline-block w-4 h-4 mr-2 -mt-1 text-red-400" />
<span>Paid application</span>
</div>
</CardContent>
</Card>
</div>
)

View File

@ -0,0 +1,170 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link";
import {ArrowDownToLine, HeartHandshake} from "lucide-react";
export const PassTab = () => (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Browser Clients</CardTitle>
<CardDescription>Manage your passwords and secure notes from your browser!</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Google Chrome</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Mozilla Firefox</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://microsoftedge.microsoft.com/addons/detail/bitwarden-password-manage/jbkfoedolllekgbhcbcoahefnbanhhlh/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Microsoft Edge</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://itunes.apple.com/app/bitwarden/id1352778147?browser=safari" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Safari (macOS)</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Opera</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Mobile Clients</CardTitle>
<CardDescription>Manage your passwords and secure notes from your mobile device!</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li className="mb-2">
<Link href="https://play.google.com/store/apps/details?id=com.bitwarden.authenticator&hl=en-US" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Android (Authenticator)</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/bitwarden-authenticator/id6497335175/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">iOS (Authenticator)</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden&hl=en-US" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Android</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744/" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">iOS</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
</ul>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-1">
<CardHeader>
<CardTitle>Desktop Clients</CardTitle>
<CardDescription>Manage your passwords and secure notes from your computer!</CardDescription>
</CardHeader>
<CardContent>
<ul>
<span className="font-bold">Linux</span>
<li className="mt-4 mb-2">
<Link href="https://bitwarden.com/download/#downloads-desktop" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Package/Archive</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://snapcraft.io/bitwarden" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Snapcraft (Ubuntu)</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-4">
<Link href="https://flathub.org/apps/com.bitwarden.desktop" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Flathub</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<span className="font-bold">Windows</span>
<li className="mt-4 mb-2">
<Link href="https://bitwarden.com/download/#downloads-desktop" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Installer/Package</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-4">
<Link href="https://www.microsoft.com/store/apps/9PJSDV0VPK04" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Microsoft Store</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<span className="font-bold">macOS</span>
<li className="mt-4 mb-2">
<Link href="https://itunes.apple.com/app/bitwarden/id1352778147" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">App Store</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
<li className="mb-2">
<Link href="https://bitwarden.com/download/#downloads-desktop" target="_blank" rel="noopener noreferrer">
<ArrowDownToLine className="inline-block w-4 h-4 mr-2 -mt-1" />
</Link>
<HeartHandshake className="inline-block w-4 h-4 mr-2 -mt-1" />
<span className="font-semibold">Installer/Brew</span>
<span className="ml-4 text-sm text-gray-600">by Bitwarden</span>
</li>
</ul>
</CardContent>
</Card>
</div>
)

View File

@ -0,0 +1,26 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
interface FeatureCardProps {
title: string
description: string
icon: React.ElementType
}
const FeatureCard = ({ title, description, icon: Icon }: FeatureCardProps) => {
return (
<Card className="bg-gray-800 border-gray-700 hover:bg-gray-700 transition-colors duration-300">
<CardHeader>
<CardTitle className="flex items-center text-white">
<Icon className="h-6 w-6 mr-2 text-blue-400" />
<span className="text-xl">{title}</span>
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-gray-300">{description}</CardDescription>
</CardContent>
</Card>
)
}
export default FeatureCard

View File

@ -1,38 +1,34 @@
'use client'; "use client"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ArrowRight } from "lucide-react" import { ArrowRight } from "lucide-react"
import { ReactTyped } from "react-typed" import { ReactTyped } from "react-typed"
import Link from "next/link";
const Hero = () => { const Hero = () => {
const phrases = ["developers", "students", "non-profits", "everyone"] const phrases = ["developers", "students", "non-profits", "everyone"]
return ( return (
<div className="bg-gray-800 py-20"> <div className="py-20 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center"> <div className="text-center">
<h1 className="text-4xl tracking-tight font-extrabold text-white sm:text-5xl md:text-6xl"> <h1 className="text-4xl tracking-tight font-extrabold sm:text-5xl md:text-6xl">
<span className="block">Free Cloud Services</span> <span className="block">Free Cloud Services</span>
<span className="block text-blue-500"> <span className="block mt-2">
for <ReactTyped for <ReactTyped strings={phrases} typeSpeed={60} backSpeed={50} loop className="text-blue-400" />
strings={phrases}
typeSpeed={60}
backSpeed={50}
loop
/> {/* there is probably a better way to format this */}
</span> </span>
</h1> </h1>
<p className="mt-3 max-w-md mx-auto text-base text-gray-300 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl"> <p className="mt-6 max-w-md mx-auto text-xl text-gray-300 sm:max-w-3xl">
Experience FOSS at its best with LibreCloud, a free service provider built with all kinds of people in mind. Experience FOSS at its best with LibreCloud, a free service provider built with all kinds of people in mind.
</p> </p>
<div className="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-8"> <div className="mt-10 max-w-md mx-auto sm:flex sm:justify-center">
<div className="rounded-md shadow"> <div className="rounded-md shadow">
<a href="#services"> <Link href="/account/login">
<Button className="w-full flex items-center justify-center py-5 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 md:py-6 md:text-lg md:px-6"> <Button className="py-6 px-8">
Get started Get started
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>
</a> </Link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,99 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { Menu, X, Server, Home, User } from "lucide-react"
const Navbar = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<nav className="bg-gray-950/70 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="relative flex items-center justify-between h-16">
<div className="flex-shrink-0 flex items-center">
<Link href="/" className="flex items-center">
<span className="text-white text-xl font-bold">LibreCloud</span>
</Link>
</div>
<div className="hidden md:block">
<div className="ml-10 flex items-center space-x-4">
<NavLink href="/" icon={Home}>
Home
</NavLink>
<NavLink href="/#services" icon={Server}>
Services
</NavLink>
<NavLink href="/account/login" icon={User}>
My Account
</NavLink>
</div>
</div>
<div className="md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
aria-expanded={isOpen}
>
<span className="sr-only">Open main menu</span>
{isOpen ? (
<X className="block h-6 w-6" aria-hidden="true" />
) : (
<Menu className="block h-6 w-6" aria-hidden="true" />
)}
</button>
</div>
</div>
</div>
{isOpen && (
<div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<MobileNavLink href="/" icon={Home}>
Home
</MobileNavLink>
<MobileNavLink href="/#services" icon={Server}>
Services
</MobileNavLink>
<MobileNavLink href="/account/login" icon={User}>
My Account
</MobileNavLink>
</div>
</div>
)}
</nav>
)
}
interface NavLinkProps {
href: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
children: React.ReactNode;
}
const NavLink: React.FC<NavLinkProps> = ({ href, icon: Icon, children }) => (
<Link
href={href}
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
>
<Icon className="mr-2 h-5 w-5" /> {children}
</Link>
)
interface MobileNavLinkProps {
href: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
children: React.ReactNode;
}
const MobileNavLink: React.FC<MobileNavLinkProps> = ({ href, icon: Icon, children }) => (
<Link
href={href}
className="flex items-center text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
<Icon className="mr-2 h-5 w-5" /> {children}
</Link>
)
export default Navbar

View File

@ -0,0 +1,11 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

50
components/ui/avatar.tsx Normal file
View File

@ -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<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

36
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

153
components/ui/command.tsx Normal file
View File

@ -0,0 +1,153 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

122
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,201 @@
"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<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

178
components/ui/form.tsx Normal file
View File

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

26
components/ui/label.tsx Normal file
View File

@ -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<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

33
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

159
components/ui/select.tsx Normal file
View File

@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

29
components/ui/switch.tsx Normal file
View File

@ -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<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

120
components/ui/table.tsx Normal file
View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

55
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react"
const MOBILE_BREAKPOINT = 768 const MOBILE_BREAKPOINT = 985
export function useIsMobile() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

8
lib/prisma.ts Normal file
View File

@ -0,0 +1,8 @@
import { PrismaClient } from "@prisma/client"
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

5
middleware.ts Normal file
View File

@ -0,0 +1,5 @@
export { auth as middleware } from "@/auth"
export const config = {
matcher: "/account/dashboard/:path*",
};

View File

@ -9,34 +9,57 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.1.5", "@hookform/resolvers": "^3.10.0",
"@radix-ui/react-progress": "^1.1.1", "@prisma/client": "^6.3.1",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/themes": "^3.2.0", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@web3icons/react": "^4.0.8",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.0",
"cookies-next": "^5.1.0", "cookies-next": "^5.1.0",
"framer-motion": "^12.1.0", "framer-motion": "^12.4.3",
"geist": "^1.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"next": "15.1.6", "next": "^15.2.0-canary.62",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.4",
"password-validator": "^5.3.0",
"prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-icons": "^5.4.0",
"react-typed": "^2.0.12", "react-typed": "^2.0.12",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"validator": "^13.12.0",
"zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.17", "@types/node": "^20.17.19",
"@types/react": "^19.0.8", "@types/react": "^19.0.9",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"eslint": "^9.19.0", "@types/validator": "^13.12.2",
"eslint": "^9.20.1",
"eslint-config-next": "15.1.6", "eslint-config-next": "15.1.6",
"postcss": "^8.5.1", "postcss": "^8.5.2",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }

17
prisma/schema.prisma Normal file
View File

@ -0,0 +1,17 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id String @id @default(cuid())
email String @unique
username String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

BIN
public/noise-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

BIN
public/noise-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

View File

@ -1,73 +1,98 @@
import type { Config } from "tailwindcss" import type { Config } from "tailwindcss"
import { fontFamily } from "tailwindcss/defaultTheme"
import tailwindcssAnimate from "tailwindcss-animate" import tailwindcssAnimate from "tailwindcss-animate"
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"], content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", "*.{js,ts,jsx,tsx,mdx}"],
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px'
}, }
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))'
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))'
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))'
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))'
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))'
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))'
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))'
}, },
}, sidebar: {
borderRadius: { DEFAULT: 'hsl(var(--sidebar-background))',
lg: "var(--radius)", foreground: 'hsl(var(--sidebar-foreground))',
md: "calc(var(--radius) - 2px)", primary: 'hsl(var(--sidebar-primary))',
sm: "calc(var(--radius) - 4px)", 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
}, accent: 'hsl(var(--sidebar-accent))',
keyframes: { 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
"accordion-down": { border: 'hsl(var(--sidebar-border))',
from: { height: "0" }, ring: 'hsl(var(--sidebar-ring))'
to: { height: "var(--radix-accordion-content-height)" }, }
}, },
"accordion-up": { borderRadius: {
from: { height: "var(--radix-accordion-content-height)" }, lg: 'var(--radius)',
to: { height: "0" }, md: 'calc(var(--radius) - 2px)',
}, sm: 'calc(var(--radius) - 4px)'
}, },
animation: { fontFamily: {
"accordion-down": "accordion-down 0.2s ease-out", sans: [
"accordion-up": "accordion-up 0.2s ease-out", '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: [tailwindcssAnimate], plugins: [tailwindcssAnimate],
} satisfies Config } satisfies Config