feat release v1.2.0: Gitea unlinking feature, improved sidebar+layout, better visual indicators and design (see more in docs)
This commit is contained in:
parent
8ff4f9be1c
commit
56448c1003
218
README.md
218
README.md
@ -1,223 +1,11 @@
|
||||
# web
|
||||
|
||||

|
||||

|
||||
[](http://creativecommons.org/publicdomain/zero/1.0/)
|
||||
[](https://github.com/ihatenodejs/librecloud-web/actions/workflows/docker.yml)
|
||||
[](https://github.com/ihatenodejs/librecloud-web/actions/workflows/bump.yml)
|
||||
|
||||
LibreCloud's website, dashboard, and API
|
||||
|
||||
## Docker Instructions
|
||||
You can also view the documentation in the `docs/` [folder](https://git.pontusmail.org/librecloud/web/src/branch/main/docs) of this repository.
|
||||
|
||||
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.
|
||||
|
||||
**Option One:** Clone Git Repo
|
||||
|
||||
```bash
|
||||
git clone https://git.pontusmail.org/librecloud/web.git
|
||||
```
|
||||
|
||||
**Option Two:** Download Compose file only
|
||||
|
||||
```bash
|
||||
wget https://git.pontusmail.org/librecloud/web/raw/branch/main/docker-compose.yml
|
||||
```
|
||||
|
||||
You may have to install `wget`, or you could use `curl` instead.
|
||||
|
||||
3. **Generate auth secret**
|
||||
|
||||
This step is relatively painless. Execute the below command to generate a `.env.local` file with an `AUTH_SECRET`.
|
||||
|
||||
```bash
|
||||
bunx auth secret
|
||||
```
|
||||
|
||||
4. **Configure environment variables**
|
||||
|
||||
Following the environment variables section of this README, update your newly created `.env.local` file with your configuration.
|
||||
|
||||
5. **Initialize Prisma**
|
||||
|
||||
Because `web` uses a database for storing Git link statuses (and other things to come),
|
||||
you will need to initialize the SQLite database.
|
||||
However, if you are using Docker Compose, a database has already been generated in the container image and is blank.
|
||||
|
||||
If you have a reason to initialize Prisma now, feel free to execute:
|
||||
|
||||
```bash
|
||||
bunx prisma migrate dev --name init
|
||||
```
|
||||
|
||||
6. **Setup environment variables**
|
||||
|
||||
Now is the time to go to the "Environment Variables" section and configure them in your `.env.local` file.
|
||||
|
||||
7. **Bring the container up**
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Please note: `sudo` may be required.
|
||||
|
||||
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.
|
||||
|
||||
8. **Complete Setup**
|
||||
|
||||
If you would like to host the entire LibreCloud frontend and backend,
|
||||
you will also need to set up 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
|
||||
|
||||
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. **Clone the repo**
|
||||
|
||||
```bash
|
||||
git clone https://git.pontusmail.org/librecloud/web.git
|
||||
cd web
|
||||
```
|
||||
|
||||
3. **Install dependencies**
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
4. **Generate auth secret**
|
||||
|
||||
This step is relatively painless. Execute the below command to generate a `.env.local` file with an `AUTH_SECRET`.
|
||||
|
||||
```bash
|
||||
bunx auth secret
|
||||
```
|
||||
|
||||
5. **Configure environment variables**
|
||||
|
||||
Following the environment variables section of this README, update your newly created `.env.local` file with your configuration.
|
||||
|
||||
6. **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
|
||||
```
|
||||
|
||||
7. **Start dev server**
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
At the time of writing, LibreCloud is not in the state of perfection,
|
||||
and as such we are expecting that you have a setup exact to ours.
|
||||
While this will change in the future, we still suggest that provide all the listed environment variables.
|
||||
|
||||
### Authentik
|
||||
|
||||
We use [Auth.js](https://authjs.dev) to provide authentication for users through Authentik.
|
||||
To do this, you will need to create a new OAuth2 provider in Authentik and put its configuration in your `.env` file.
|
||||
|
||||
If you need more help doing this, there is a fantastic guide [on Authentik's wiki](https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/).
|
||||
|
||||
| Environment Variable | Description | Example |
|
||||
|-----------------------|---------------------------------------------------------|-------------------------------------------------|
|
||||
| AUTH_AUTHENTIK_ID | (Auth.js) OAuth2 Provider - Client ID | `UHEkjdUIqi938hUIEijdkWZiudhIUshefIJIo8u3u` |
|
||||
| AUTH_AUTHENTIK_SECRET | (Auth.js) OAuth2 Provider - Client Secret | [long string] |
|
||||
| AUTH_AUTHENTIK_ISSUER | (Auth.js) OAuth2 Provider - OpenID Configuration Issuer | `http://authentik.local/application/o/example/` |
|
||||
| AUTHENTIK_API_KEY | API key for authenticating with Authentik's API | N/A |
|
||||
| AUTHENTIK_API_URL | Authentik's API endpoint URL | `http://authentik.local/api/v3` |
|
||||
|
||||
### Gitea
|
||||
|
||||
Next, you will need to configure `web` with your Gitea instance.
|
||||
Create a new access token in your Gitea user settings (),
|
||||
and input the key you receive, as well as the URL of your instance, and the API URL.
|
||||
You can find a link to the API and its endpoint URL on the footer.
|
||||
|
||||
| Environment Variable | Description | Example |
|
||||
|----------------------|-----------------------------------------------|--------------------------------------------|
|
||||
| GITEA_API_URL | Your Gitea instance API endpoint (see footer) | `http://gitea.local/api/v1` |
|
||||
| GITEA_API_KEY | Access Token created in user settings | `0000000000000000000000000000000000000000` |
|
||||
| GITEA_URL | Your Gitea instance URL | `http://gitea.local` |
|
||||
|
||||
### mail-connect
|
||||
|
||||
mail-connect, another project by LibreCloud, is a bridge from `docker-mailserver` to an API. It talks to the container via a Docker socket, but you will need to tell `web` where to find your mailserver API.
|
||||
|
||||
Keep in mind, this endpoint should **NOT** be public, and `web` should be the only authorized user of the API, unless you know what you're doing. There is zero authentication.
|
||||
|
||||
| Environment Variable | Description | Example |
|
||||
|----------------------|------------------------------|-------------------------|
|
||||
| MAIL_CONNECT_API_URL | URL of your mail-connect API | `http://localhost:4200` |
|
||||
|
||||
### Auth.js
|
||||
|
||||
We suggest starting by allowing Auth.js
|
||||
|
||||
| Environment Variable | Description | Example |
|
||||
|----------------------|---------------------------------------------------|-----------------------------------------------------------------------|
|
||||
| AUTH_SECRET | Generated during `.env.local` creation | `R98/+7HbakYa73YHbooAND+nzae8RaudOdq8Uab/suE=` |
|
||||
| AUTH_TRUST_HOST | Required, should always be set to `true` | `true` |
|
||||
| NEXTAUTH_URL | The URL LibreCloud will be publicly accessible at | `http://localhost:3000` (testing), `https://example.com` (production) |
|
||||
|
||||
### Cloudflare
|
||||
|
||||
We use Cloudflare Turnstile for detecting bots and automated scripts attempting to abuse our services. We chose it because it's the perfect balance of security and convenience for users. It was also the most preferred option in the [poll we ran on my Telegram channel](https://t.me/pontushub/457).
|
||||
|
||||
You can get the keys you need for Cloudflare Turnstile [here](https://www.cloudflare.com/application-services/products/turnstile/). It's very plug and play.
|
||||
|
||||
If you would like to simply test or bypass Cloudflare Turnstile, you can use one of the site keys provided [here](https://developers.cloudflare.com/turnstile/troubleshooting/testing/) instead of your own.
|
||||
|
||||
| Environment Variable | Description | Example |
|
||||
|------------------------|-------------------------------------------|---------------------------------------|
|
||||
| NEXT_PUBLIC_CF_SITEKEY | Cloudflare Turnstile site key (public) | `1x00000000000000000000AA` |
|
||||
| CF_SECRETKEY | Cloudflare Turnstile secret key (private) | `0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Performing a database migration
|
||||
|
||||
In case of an update to `prisma/schema.prisma` in this repo, you should run the below command to migrate the old database.
|
||||
|
||||
Each update to this file is guaranteed to work with the previous version of the file to ensure maximum compatibility. While every effort has been made to ensure compatibility, we are not responsible for any data loss.
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev --name update-schema # Migrate
|
||||
npx prisma migrate deploy # Deploy
|
||||
```
|
||||
|
||||
## To-Do
|
||||
|
||||
* [ ] Add theme switcher to home page
|
||||
* [ ] Implement security scans
|
||||
* [ ] Rate-limiting on API
|
||||
## <div style="text-align: center;">[Documentation](https://docs.librecloud.cc) | [Donate](https://donate.stripe.com/6oE8yxaPk6yXbpS145)</div>
|
@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { Sparkles } from "lucide-react"
|
||||
|
||||
const fadeIn = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
@ -12,26 +11,21 @@ const fadeIn = {
|
||||
|
||||
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'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>
|
||||
<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'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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
@ -6,7 +6,6 @@ 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 },
|
||||
@ -16,36 +15,29 @@ const fadeIn = {
|
||||
|
||||
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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
|
||||
import { BadgeDollarSign } from "lucide-react"
|
||||
|
||||
const fadeIn = {
|
||||
@ -12,26 +11,21 @@ const fadeIn = {
|
||||
|
||||
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}>
|
||||
<div className="flex justify-center">
|
||||
<h3 className="text-xl font-bold mb-2 uppercase bg-slate-700 rounded px-2">Coming Soon</h3>
|
||||
</div>
|
||||
<h1 className="text-6xl text-center font-bold">Exchange Crypto</h1>
|
||||
<div className="flex justify-center mt-16">
|
||||
<BadgeDollarSign size={69} />
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-4xl mt-6">We find the best price, seriously</h2>
|
||||
<p className="mt-4">We use the API from several providers to provide comparisons between the different providers. You complete the exchange via our exchange interface (via provider) or through the provider's website. Each time, you can walk out knowing you got the best deal. The best part? We don't take a cut or make a profit off your usage.</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<motion.div {...fadeIn}>
|
||||
<div className="flex justify-center">
|
||||
<h3 className="text-xl font-bold mb-2 uppercase bg-slate-700 rounded px-2">Coming Soon</h3>
|
||||
</div>
|
||||
|
||||
<h1 className="text-6xl text-center font-bold">Exchange Crypto</h1>
|
||||
<div className="flex justify-center mt-16">
|
||||
<BadgeDollarSign size={69} />
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-4xl mt-6">We find the best price, seriously</h2>
|
||||
<p className="mt-4">We use the API from several providers to provide comparisons between the different providers. You complete the exchange via our exchange interface (via provider) or through the provider's website. Each time, you can walk out knowing you got the best deal. The best part? We don't take a cut or make a profit off your usage.</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,21 @@
|
||||
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";
|
||||
import { Header } from "@/components/pages/dashboard/Header"
|
||||
import { ServerSideMenu } from "@/components/pages/dashboard/ServerSideMenu"
|
||||
import { auth } from "@/auth"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
const DashboardLayout = async ({ children }: { children: ReactNode }) => {
|
||||
// Server-side auth check
|
||||
const session = await auth()
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!session) {
|
||||
redirect("/account/login")
|
||||
}
|
||||
|
||||
const DashboardLayout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
@ -13,21 +23,21 @@ const DashboardLayout = ({ children }: { children: ReactNode }) => {
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="min-h-screen flex flex-col bg-background text-foreground">
|
||||
<div className="grow">
|
||||
<SidebarProvider>
|
||||
<div className="flex flex-col w-full min-h-screen bg-background">
|
||||
<div className="min-h-screen w-full flex flex-col bg-background text-foreground">
|
||||
<SidebarProvider>
|
||||
<div className="flex w-full h-screen overflow-hidden">
|
||||
<ServerSideMenu />
|
||||
<div className="flex-1 w-full flex flex-col overflow-hidden">
|
||||
<Header />
|
||||
{children}
|
||||
<div className="lg:ml-64">
|
||||
<Footer />
|
||||
</div>
|
||||
<main className="flex-1 w-full overflow-y-auto">
|
||||
<div className="p-8 w-full">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<div className="fixed bottom-4 left-4">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
@ -7,7 +7,6 @@ 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 = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
@ -46,10 +45,22 @@ export default function Dashboard() {
|
||||
gitEmail: data.email || "",
|
||||
}))
|
||||
} else {
|
||||
throw new Error("Failed to fetch Gitea account details");
|
||||
setDashboardState((prev) => ({
|
||||
...prev,
|
||||
gitUser: "Unlinked",
|
||||
gitAvatar: "",
|
||||
gitLastLogin: "Never",
|
||||
}))
|
||||
throw new Error("Failed to fetch Gitea account details")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching your Gitea user data:", error);
|
||||
setDashboardState((prev) => ({
|
||||
...prev,
|
||||
gitUser: "Unlinked",
|
||||
gitAvatar: "",
|
||||
gitLastLogin: "Never",
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,36 +68,29 @@ export default function Dashboard() {
|
||||
}, [])
|
||||
|
||||
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">Dashboard</h1>
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="mb-4 flex flex-wrap">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
<TabsTrigger value="services">Services</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<OverviewTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<SecurityTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="services">
|
||||
<ServicesTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="git">
|
||||
<GitTab dashboardState={dashboardState} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<motion.div {...fadeIn}>
|
||||
<h1 className="text-3xl font-bold mb-6 text-foreground">Dashboard</h1>
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="mb-4 flex flex-wrap">
|
||||
<TabsTrigger value="overview" className="cursor-pointer">Overview</TabsTrigger>
|
||||
<TabsTrigger value="security" className="cursor-pointer">Security</TabsTrigger>
|
||||
<TabsTrigger value="services" className="cursor-pointer">Services</TabsTrigger>
|
||||
<TabsTrigger value="git" className="cursor-pointer">Git</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<OverviewTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<SecurityTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="services">
|
||||
<ServicesTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="git">
|
||||
<GitTab dashboardState={dashboardState} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
@ -21,23 +20,23 @@ export default function Settings() {
|
||||
hideUpgrades: false,
|
||||
hideCrypto: false
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/users/settings');
|
||||
const response = await fetch('/api/users/settings')
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings(data);
|
||||
const data = await response.json()
|
||||
setSettings(data)
|
||||
} else {
|
||||
console.error('[!] Failed to fetch settings');
|
||||
console.error('[!] Failed to fetch settings')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[!] Error fetching settings:', error);
|
||||
console.error('[!] Error fetching settings:', error)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
@ -48,10 +47,10 @@ export default function Settings() {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[settingName]: value
|
||||
}));
|
||||
}))
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setLoading(true)
|
||||
const response = await fetch('/api/users/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -61,86 +60,77 @@ export default function Settings() {
|
||||
...settings,
|
||||
[settingName]: value
|
||||
}),
|
||||
});
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const updatedSettings = await response.json();
|
||||
setSettings(updatedSettings);
|
||||
const updatedSettings = await response.json()
|
||||
setSettings(updatedSettings)
|
||||
} else {
|
||||
console.error('[!] Failed to update settings');
|
||||
console.error('[!] Failed to update settings')
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[settingName]: !value
|
||||
}));
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[!] Error updating settings:', error);
|
||||
console.error('[!] Error updating settings:', error)
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[settingName]: !value
|
||||
}));
|
||||
}))
|
||||
} finally {
|
||||
setLoading(false);
|
||||
window.location.reload();
|
||||
setLoading(false)
|
||||
window.location.reload()
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<ChangePassword />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<LayoutDashboard size={15} className="mr-1" />
|
||||
UI Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Modify your user experience here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="hide-ai">Hide Generative AI</Label>
|
||||
<Switch
|
||||
id="hide-ai"
|
||||
checked={settings.hideGenAI}
|
||||
disabled={loading}
|
||||
onCheckedChange={(checked) => updateSetting('hideGenAI', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="hide-upgrades">Hide all upgrades/roles</Label>
|
||||
<Switch
|
||||
id="hide-upgrades"
|
||||
checked={settings.hideUpgrades}
|
||||
disabled={loading}
|
||||
onCheckedChange={(checked) => updateSetting('hideUpgrades', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="hide-crypto">Hide crypto exchange</Label>
|
||||
<Switch
|
||||
id="hide-crypto"
|
||||
checked={settings.hideCrypto}
|
||||
disabled={loading}
|
||||
onCheckedChange={(checked) => updateSetting('hideCrypto', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<motion.div {...fadeIn}>
|
||||
<h1 className="text-3xl font-bold mb-6 text-foreground">Settings</h1>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<ChangePassword />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<LayoutDashboard size={15} className="mr-1" />
|
||||
Dashboard Settings
|
||||
</CardTitle>
|
||||
<CardDescription>Customize your dashboard experience</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="hideGenAI">Hide Generative AI</Label>
|
||||
<Switch
|
||||
id="hideGenAI"
|
||||
checked={settings.hideGenAI}
|
||||
onCheckedChange={(checked) => updateSetting('hideGenAI', checked)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="hideUpgrades">Hide Upgrades</Label>
|
||||
<Switch
|
||||
id="hideUpgrades"
|
||||
checked={settings.hideUpgrades}
|
||||
onCheckedChange={(checked) => updateSetting('hideUpgrades', checked)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="hideCrypto">Hide Crypto Exchange</Label>
|
||||
<Switch
|
||||
id="hideCrypto"
|
||||
checked={settings.hideCrypto}
|
||||
onCheckedChange={(checked) => updateSetting('hideCrypto', checked)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
const fadeIn = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
@ -13,65 +12,54 @@ const fadeIn = {
|
||||
|
||||
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>How much it costs us to run LibreCloud each month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-sm"><span className="font-bold">Month of:</span> March</span>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold uppercase">Server</span>
|
||||
<span className="font-bold uppercase">Price</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">node0.librecloud.cc</span>
|
||||
<span>$28.88</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">orbit.librecloud.cc</span>
|
||||
<span>$34.24</span>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold uppercase">Domains</span>
|
||||
<span className="font-bold uppercase">Price</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">0 Domains</span>
|
||||
<span>$0.00</span>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold uppercase">Addons</span>
|
||||
<span className="font-bold uppercase">Price</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">0GB Disk Space</span>
|
||||
<span>$0.00</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">0GB RAM</span>
|
||||
<span>$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="font-bold">$63.12</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<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>How much it costs us to run LibreCloud each month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-sm"><span className="font-bold">Month of:</span> March</span>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold uppercase">Server</span>
|
||||
<span className="font-bold uppercase">Price</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">librecloud.cc (PowerEdge R630)</span>
|
||||
<span>$64.99</span>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold uppercase">Domains</span>
|
||||
<span className="font-bold uppercase">Price</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">0 Domains</span>
|
||||
<span>$0.00</span>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold uppercase">Addons</span>
|
||||
<span className="font-bold uppercase">Price</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">0GB Disk Space</span>
|
||||
<span>$0.00</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-sm">0GB RAM</span>
|
||||
<span>$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="font-bold">$64.99</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@ -15,47 +14,40 @@ const fadeIn = {
|
||||
|
||||
export default function Support() {
|
||||
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">Support</h1>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="col-span-full md:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-2xl">
|
||||
<Mail size={24} className="mr-1.5" />
|
||||
Email
|
||||
</CardTitle>
|
||||
<CardDescription>Create a ticket by sending an email</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-sm">You can either send a message to the address below, or click the button.</span>
|
||||
<div className="flex w-full max-w-sm items-center space-x-2 mt-2">
|
||||
<Input value="support@librecloud.cc" disabled />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => navigator.clipboard.writeText(process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@librecloud.cc") }
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => window.location.href = 'mailto:' + (process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@librecloud.cc")}
|
||||
>
|
||||
<ExternalLink />
|
||||
Open in Email Client
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<motion.div {...fadeIn}>
|
||||
<h1 className="text-3xl font-bold mb-6 text-foreground">Support</h1>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="col-span-full md:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-2xl">
|
||||
<Mail size={24} className="mr-1.5" />
|
||||
Email
|
||||
</CardTitle>
|
||||
<CardDescription>Create a ticket by sending an email</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-sm">You can either send a message to the address below, or click the button.</span>
|
||||
<div className="flex w-full max-w-sm items-center space-x-2 mt-2">
|
||||
<Input value="support@librecloud.cc" disabled />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => navigator.clipboard.writeText(process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@librecloud.cc") }
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => window.location.href = 'mailto:' + (process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@librecloud.cc")}
|
||||
>
|
||||
<ExternalLink />
|
||||
Open in Email Client
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { motion } from "motion/react"
|
||||
import { SideMenu } from "@/components/pages/dashboard/SideMenu"
|
||||
|
||||
const fadeIn = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
@ -11,17 +10,10 @@ const fadeIn = {
|
||||
|
||||
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>
|
||||
<motion.div {...fadeIn}>
|
||||
<h1 className="text-3xl font-bold mb-6 text-foreground">Upgrades</h1>
|
||||
<p>Coming soon</p>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ export async function POST(request: Request) {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json({ error: "Failed to fetch Git user data" }, { status: response.status })
|
||||
return NextResponse.json({ error: "Failed to fetch Git user data" }, { status: 403 })
|
||||
}
|
||||
|
||||
const userData = await response.json()
|
||||
|
35
app/api/git/unlink/route.ts
Normal file
35
app/api/git/unlink/route.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { auth } from "@/auth"
|
||||
import { NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST() {
|
||||
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 dbUsrCheck = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
})
|
||||
|
||||
if (dbUsrCheck && dbUsrCheck.username) {
|
||||
await prisma.user.update({
|
||||
where: { email },
|
||||
data: { username: null },
|
||||
})
|
||||
return NextResponse.json({ success: true })
|
||||
} else if (!dbUsrCheck?.username) {
|
||||
return NextResponse.json({ error: "Database error" }, { status: 500 })
|
||||
} else {
|
||||
return NextResponse.json({ error: "Git account not linked" }, { status: 404 })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Git unlink API error:", error)
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session || !session.user?.email) {
|
||||
if (!session || !session.user || !session.user.email) {
|
||||
return NextResponse.json({ error: "Unauthorized - Please login first" }, { status: 401 })
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Mail, Key, Loader } from "lucide-react"
|
||||
import { Mail, Key, Loader2 } from "lucide-react"
|
||||
|
||||
export function ChangePassword() {
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
@ -62,8 +62,9 @@ export function ChangePassword() {
|
||||
className="mt-1.5"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? <><Loader className="animate-spin" /> Changing...</> : <><Key /> Change Password</>}
|
||||
<Button type="submit" disabled={loading || newPassword.length < 8}>
|
||||
{/* TODO: this should have the usual error message style */
|
||||
loading ? <><Loader2 className="animate-spin" /> Changing...</> : <><Key /> Change Password</>}
|
||||
</Button>
|
||||
{message && <p className="text-sm text-center">{message}</p>}
|
||||
</form>
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Users, Clock, User, Mail } from "lucide-react"
|
||||
@ -24,17 +24,7 @@ export function GiteaProfileCard({ dashboardState }: { dashboardState: Dashboard
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden transition-all hover:shadow-lg">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl font-semibold">Profile</CardTitle>
|
||||
{dashboardState.gitIsAdmin && (
|
||||
<Badge variant="secondary" className="h-6">
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<CardContent className="space-y-6 mt-6">
|
||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:gap-8">
|
||||
<Avatar className="h-24 w-24 shrink-0 border-2 border-border">
|
||||
<AvatarImage src={dashboardState.gitAvatar || ""} alt={dashboardState.gitUser} />
|
||||
@ -43,7 +33,14 @@ export function GiteaProfileCard({ dashboardState }: { dashboardState: Dashboard
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-bold tracking-tight">{dashboardState.gitUser}</h3>
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-2xl font-bold tracking-tight">{dashboardState.gitUser}</h3>
|
||||
{dashboardState.gitIsAdmin && (
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
|
||||
|
@ -9,7 +9,9 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { SiGitea } from "react-icons/si"
|
||||
import { Loader } from "lucide-react"
|
||||
import { AlertCircle, Loader2 } from "lucide-react"
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
|
||||
import Link from "next/link"
|
||||
|
||||
const giteaFormSchema = z.object({
|
||||
username: z
|
||||
@ -28,6 +30,9 @@ type GiteaFormValues = z.infer<typeof giteaFormSchema>
|
||||
|
||||
export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [unlinkLoading, setUnlinkLoading] = useState(false)
|
||||
const [linkError, setLinkError] = useState("")
|
||||
const [unlinkError, setUnlinkError] = useState("")
|
||||
|
||||
const form = useForm<GiteaFormValues>({
|
||||
resolver: zodResolver(giteaFormSchema),
|
||||
@ -47,28 +52,48 @@ export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
body: JSON.stringify({ username: data.username }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to link Gitea account")
|
||||
}
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.success) {
|
||||
console.log("Gitea account linked:", responseData)
|
||||
location.reload()
|
||||
} else if (responseData.error) {
|
||||
form.setError("username", { message: responseData.error })
|
||||
setLinkError(responseData.error)
|
||||
setLoading(false)
|
||||
} else {
|
||||
form.setError("username", { message: "Failed to link" })
|
||||
setLinkError("Failed to link")
|
||||
setLoading(false)
|
||||
throw new Error("Failed to link Gitea account")
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
setLinkError("Failed to link")
|
||||
console.error("Error linking Gitea account:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const onUnlink = async () => {
|
||||
setUnlinkLoading(true)
|
||||
try {
|
||||
const response = await fetch("/api/git/unlink", {
|
||||
method: "POST",
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.success) {
|
||||
console.log("Gitea account unlinked")
|
||||
location.reload()
|
||||
} else {
|
||||
setUnlinkError(responseData.error)
|
||||
console.error("Failed to unlink:", responseData.error)
|
||||
setUnlinkLoading(false)
|
||||
}
|
||||
} catch (error) {
|
||||
setUnlinkLoading(false)
|
||||
setUnlinkError("Failed to unlink")
|
||||
console.error("Error unlinking Gitea account:", error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!linked) {
|
||||
return (
|
||||
<Card>
|
||||
@ -82,6 +107,13 @@ export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
{linkError && (
|
||||
<Alert variant="destructive" className="text-red-500 mb-4">
|
||||
<AlertCircle color={"#EF4444"} size={18} />
|
||||
<AlertTitle className="text-lg font-bold">Oops! Something went wrong.</AlertTitle>
|
||||
<AlertDescription>{linkError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
@ -90,7 +122,7 @@ export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea Username</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl className="mt-1">
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -98,15 +130,29 @@ export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
)}
|
||||
/>
|
||||
{loading ? (
|
||||
<Button disabled>
|
||||
<Loader className="animate-spin" />
|
||||
Linking...
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button className="w-full" disabled>
|
||||
<Loader2 className="animate-spin" />
|
||||
Linking...
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" disabled>
|
||||
<SiGitea />
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="submit">
|
||||
<SiGitea />
|
||||
Link with Gitea
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" className="w-1/2 cursor-pointer">
|
||||
<SiGitea />
|
||||
Link with Gitea
|
||||
</Button>
|
||||
<a href="https://try.gitea.com" target="_blank" className="w-1/2">
|
||||
<Button type="button" variant="outline" className="w-full cursor-pointer">
|
||||
<SiGitea />
|
||||
Create Account
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
@ -115,7 +161,39 @@ export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
</Card>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Git Link</CardTitle>
|
||||
<CardDescription>
|
||||
Your Gitea account is currently linked to your LibreCloud account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{unlinkError && (
|
||||
<Alert variant="destructive" className="text-red-500 mb-4">
|
||||
<AlertCircle color={"#EF4444"} size={18} />
|
||||
<AlertTitle className="text-lg font-bold">Oops! Something went wrong.</AlertTitle>
|
||||
<AlertDescription>{unlinkError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<p className="text-sm mb-4">
|
||||
Unlinking your Gitea account will not delete your Gitea account. You can delete your Gitea account <Link href="https://try.gitea.com/user/sign_up" target="_blank" className="text-blue-500">here</Link>.
|
||||
</p>
|
||||
{unlinkLoading ? (
|
||||
<Button variant="destructive" disabled>
|
||||
<Loader2 className="animate-spin" />
|
||||
Unlinking...
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="destructive" onClick={onUnlink}>
|
||||
<SiGitea />
|
||||
Unlink Gitea Account
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,30 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
|
||||
import { DoorOpen } from "lucide-react";
|
||||
import { logout } from "@/actions/logout";
|
||||
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"
|
||||
import { DoorOpen, Loader2 } from "lucide-react"
|
||||
import { logout } from "@/actions/logout"
|
||||
import { useState } from "react"
|
||||
|
||||
export function SidebarSignOut() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<form action={logout}>
|
||||
<SidebarMenuButton type="submit">
|
||||
<DoorOpen />
|
||||
<span>Logout</span>
|
||||
{isLoading ? (
|
||||
<form action={logout}>
|
||||
<SidebarMenuButton type="submit" className="cursor-pointer" onClick={() => setIsLoading(true)}>
|
||||
<DoorOpen />
|
||||
<span>Logout</span>
|
||||
</SidebarMenuButton>
|
||||
</form>
|
||||
) : (
|
||||
<SidebarMenuButton type="button" disabled>
|
||||
<Loader2 className="animate-spin" />
|
||||
<span>Logging out...</span>
|
||||
</SidebarMenuButton>
|
||||
</form>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default SidebarSignOut;
|
||||
export default SidebarSignOut
|
@ -1,25 +0,0 @@
|
||||
"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;
|
||||
|
@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { useEffect, useState } from "react"
|
||||
import { TbNoCopyright } from "react-icons/tb";
|
||||
import { TbNoCopyright } from "react-icons/tb"
|
||||
|
||||
export function Footer() {
|
||||
const [renderTime, setRenderTime] = useState<number | null>(null)
|
||||
@ -19,22 +20,27 @@ export function Footer() {
|
||||
|
||||
return (
|
||||
<footer className="py-2 px-4 text-sm text-muted-foreground bg-muted">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center justify-center">
|
||||
<TbNoCopyright className="mr-2" />
|
||||
<p className="text-center md:text-left">
|
||||
Created by a community, not a company.
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<SidebarTrigger variant="secondary" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 md:flex-row md:items-center md:justify-end md:gap-4">
|
||||
<div className="flex items-center">
|
||||
<TbNoCopyright className="mr-2" />
|
||||
<p className="text-end text-xs md:text-sm">
|
||||
Created by a community, not a company.
|
||||
</p>
|
||||
</div>
|
||||
{renderTime !== null ? (
|
||||
<p className="hidden text-end md:block md:text-sm">
|
||||
Page rendered in {renderTime.toFixed(2)} ms
|
||||
</p>
|
||||
) : (
|
||||
<p className="hidden text-end md:block md:text-sm">
|
||||
Calculating render time...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{renderTime !== null ? (
|
||||
<p className="text-center md:text-right">
|
||||
Page rendered in {renderTime.toFixed(2)} ms
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center md:text-right">
|
||||
Calculating render time...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
@ -24,7 +24,7 @@ export const Header = async () => {
|
||||
]
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 h-16 z-20 bg-background border-b p-4 flex items-center justify-between">
|
||||
<header className="sticky top-0 h-16 z-30 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)) ? (
|
||||
|
39
components/pages/dashboard/ServerSideMenu.tsx
Normal file
39
components/pages/dashboard/ServerSideMenu.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { auth } from "@/auth"
|
||||
import { SideMenu } from "./SideMenu"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function ServerSideMenu() {
|
||||
// Server-side auth check
|
||||
const session = await auth()
|
||||
|
||||
if (!session || !session.user || !session.user.email) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch user settings
|
||||
const userSettings = await fetchUserSettings(session.user.email)
|
||||
|
||||
return (
|
||||
<SideMenu
|
||||
initialSettings={userSettings}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchUserSettings(email: string) {
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (user) {
|
||||
return {
|
||||
hideGenAI: user.hideGenAI,
|
||||
hideUpgrades: user.hideUpgrades,
|
||||
hideCrypto: user.hideCrypto,
|
||||
}
|
||||
} else {
|
||||
const newUser = await prisma.user.create({ data: { email } });
|
||||
return {
|
||||
hideGenAI: newUser.hideGenAI,
|
||||
hideUpgrades: newUser.hideUpgrades,
|
||||
hideCrypto: newUser.hideCrypto,
|
||||
}
|
||||
}
|
||||
}
|
@ -20,19 +20,37 @@ import {
|
||||
SidebarGroupLabel,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import LogoutMenuItem from "@/components/custom/LogoutMenuItem"
|
||||
import type React from "react"
|
||||
import Link from "next/link"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
export const SideMenu: React.FC = () => {
|
||||
const [hideGenAI, setHideGenAI] = useState(true)
|
||||
const [hideUpgrades, setHideUpgrades] = useState(true)
|
||||
const [hideCrypto, setHideCrypto] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
interface UserSettings {
|
||||
hideGenAI: boolean
|
||||
hideUpgrades: boolean
|
||||
hideCrypto: boolean
|
||||
}
|
||||
|
||||
interface SideMenuProps {
|
||||
initialSettings?: UserSettings
|
||||
}
|
||||
|
||||
export const SideMenu: React.FC<SideMenuProps> = ({ initialSettings }) => {
|
||||
const [hideGenAI, setHideGenAI] = useState(initialSettings?.hideGenAI ?? true)
|
||||
const [hideUpgrades, setHideUpgrades] = useState(initialSettings?.hideUpgrades ?? true)
|
||||
const [hideCrypto, setHideCrypto] = useState(initialSettings?.hideCrypto ?? true)
|
||||
const [isLoading, setIsLoading] = useState(!initialSettings)
|
||||
const { setOpenMobile } = useSidebar()
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch settings if they weren't provided by the server
|
||||
if (initialSettings) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
fetch("/api/users/settings")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
@ -45,133 +63,141 @@ export const SideMenu: React.FC = () => {
|
||||
console.error("Failed to fetch user settings:", error)
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [])
|
||||
}, [initialSettings])
|
||||
|
||||
// Handler to close mobile sidebar when a link is clicked
|
||||
const handleLinkClick = () => {
|
||||
setOpenMobile(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed left-0 top-16 h-[calc(100vh-4rem)] w-64 border-r bg-background z-10 hidden lg:block">
|
||||
<Sidebar className="h-full pt-16">
|
||||
<SidebarContent className="h-full bg-background">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Services</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard">
|
||||
<LayoutDashboard />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<Sidebar className="h-full">
|
||||
<SidebarContent className="h-full bg-background">
|
||||
<div className="flex items-center justify-center pt-6">
|
||||
<h3 className="text-2xl font-bold text-primary">
|
||||
LibreCloud
|
||||
</h3>
|
||||
</div>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Services</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard" onClick={handleLinkClick}>
|
||||
<LayoutDashboard />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
{isLoading ? (
|
||||
{isLoading ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuSkeleton showIcon />
|
||||
</SidebarMenuItem>
|
||||
) : (
|
||||
!hideGenAI && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuSkeleton showIcon />
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/ai" onClick={handleLinkClick}>
|
||||
<Sparkle />
|
||||
<span>Generative AI</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : (
|
||||
!hideGenAI && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/ai">
|
||||
<Sparkle />
|
||||
<span>Generative AI</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)}
|
||||
)
|
||||
)}
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/downloads" onClick={handleLinkClick}>
|
||||
<HardDriveDownload />
|
||||
<span>Download Center</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Tools</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{isLoading ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/downloads">
|
||||
<HardDriveDownload />
|
||||
<span>Download Center</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuSkeleton showIcon />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Tools</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{isLoading ? (
|
||||
) : (
|
||||
!hideCrypto && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuSkeleton showIcon />
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/exchange" onClick={handleLinkClick}>
|
||||
<Bitcoin />
|
||||
<span>Exchange Crypto</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : (
|
||||
!hideCrypto && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/exchange">
|
||||
<Bitcoin />
|
||||
<span>Exchange Crypto</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)}
|
||||
)
|
||||
)}
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/statistics" onClick={handleLinkClick}>
|
||||
<BarChartIcon />
|
||||
<span>Statistics</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Account</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{isLoading ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/statistics">
|
||||
<BarChartIcon />
|
||||
<span>Statistics</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuSkeleton showIcon />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Account</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{isLoading ? (
|
||||
) : (
|
||||
!hideUpgrades && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuSkeleton showIcon />
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/upgrades" onClick={handleLinkClick}>
|
||||
<Crown />
|
||||
<span>Upgrades</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : (
|
||||
!hideUpgrades && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/upgrades">
|
||||
<Crown />
|
||||
<span>Upgrades</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)}
|
||||
)
|
||||
)}
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/support">
|
||||
<Headset />
|
||||
<span>Support</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/support" onClick={handleLinkClick}>
|
||||
<Headset />
|
||||
<span>Support</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/settings">
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/account/dashboard/settings" onClick={handleLinkClick}>
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<LogoutMenuItem />
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</div>
|
||||
<LogoutMenuItem />
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -17,3 +17,7 @@ You will need to setup the following services with Docker, and route them proper
|
||||
## Get Started
|
||||
|
||||
For the best experience, we suggest you use a [Docker setup](getstarted/docker.md) in production, and a [Dev environment](getstarted/dev.md) while testing/making changes.
|
||||
|
||||
## Donations
|
||||
|
||||
A lot of time and resources are put into operating LibreCloud. Consider donating to support our work with [Stripe](https://donate.stripe.com/6oE8yxaPk6yXbpS145).
|
@ -12,3 +12,7 @@
|
||||
* [Environment Variables](reference/env.md)
|
||||
* [Database Migration Guide](reference/db-migration.md)
|
||||
* [Editing Documentation](reference/editing-docs.md)
|
||||
|
||||
* Updates
|
||||
|
||||
* [v1.2.0](updates/1.2.0.md)
|
BIN
docs/img/1.2.0-1.png
Normal file
BIN
docs/img/1.2.0-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
BIN
docs/img/1.2.0-2.png
Normal file
BIN
docs/img/1.2.0-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
docs/img/1.2.0-3.png
Normal file
BIN
docs/img/1.2.0-3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
40
docs/updates/1.2.0.md
Normal file
40
docs/updates/1.2.0.md
Normal file
@ -0,0 +1,40 @@
|
||||
# web v1.2.0
|
||||
|
||||
## What's new?
|
||||
|
||||
### Additions
|
||||
|
||||
- Gitea account unlinking card to the 'Git' tab on the dashboard
|
||||
- Visual "Logging out..." display to sidebar upon click
|
||||
- "Create Account" button on Gitea link card
|
||||
- Allow disabling of signup via an environment variable
|
||||
|
||||
### Updates
|
||||
|
||||
- Updated statistics page with latest costs
|
||||
- Sidebar button now inside the footer for better layout and style
|
||||
- More elements use `Loader2` instead of `Loader` (better look in my opinion)
|
||||
- Improved Gitea link/unlink API logic
|
||||
|
||||
### Improvements
|
||||
|
||||
- Layout changes to sidebar and user control header
|
||||
- Better padding and margins on page content
|
||||
- Moved settings fetch to server action
|
||||
- Significantly improved sidebar load times in testing
|
||||
- Authentication and session checking logic
|
||||
- Footer design and adaptability tweaks
|
||||
- Removed card title from Git card on dashboard
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed [#3 - Sidebar on dashboard not closing on desktop](https://git.pontusmail.org/librecloud/web/issues/3)
|
||||
- Fixed [#4 - Stop throwing errors for valid responses on Git linking](https://git.pontusmail.org/librecloud/web/issues/4)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
@ -33,7 +33,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"motion": "^12.6.3",
|
||||
"next": "^15.2.4",
|
||||
"next": "^15.2.5",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-themes": "^0.4.6",
|
||||
"next-turnstile": "^1.0.2",
|
||||
|
Loading…
x
Reference in New Issue
Block a user