migration

This commit is contained in:
Théo LAGACHE
2025-05-16 15:54:17 +02:00
parent 01019fe1a3
commit 6f2523e524
285 changed files with 15492 additions and 46090 deletions
+33
View File
@@ -0,0 +1,33 @@
# Environnement
NODE_ENV=production
# Base de données
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Projet
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Type (s3, local)
STORAGE_TYPE=local
+3
View File
@@ -0,0 +1,3 @@
{
"WillLuke.nextjs.hasPrompted": true
}
+1 -1
View File
@@ -1,2 +1,2 @@
migrate:
docker compose run --rm app sh -c "npx prisma migrate dev"
docker compose run --rm app
+157 -116
View File
@@ -1,21 +1,15 @@
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://portabase.io">
<img src="images/logo.png" alt="Logo" width="80" height="80">
</a>
[//]: # (<br />)
[//]: # (<div align="center">)
[//]: # ( <a href="https://github.com/othneildrew/Best-README-Template">)
[//]: # ( <img src="images/logo.png" alt="Logo" width="80" height="80">)
[//]: # ( </a>)
<h3 align="center">Portabase</h3>
<h3 align="center">Portabase</h3>
<p align="center">
Backup / Restore your databases instances
Easily backup and restore your database instances with Portabase
<br />
<a href="https://portabase.io"><strong>Explore the docs »</strong></a>
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
<br />
<br />
<a href="https://portabase.io">View Demo</a>
@@ -24,159 +18,206 @@
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</p>
</div>
## 📚 Table of Contents
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
- [Acknowledgments](#acknowledgments)
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
---
<!-- ABOUT THE PROJECT -->
## About The Project
Portabase is the server dashboard service working with portabase agents.
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
### 🔧 Built With
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- BetterAuth
- React Email
### Built With
---
* [![NextJS][NextJS]][NextJS-url]
* [![Prisma][Prisma]][Prisma-url]
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
## 🚀 Getting Started
### Prerequisites
Install docker on your machine
Ensure Docker is installed on your machine before getting started.
### Installation
Installation steps in order to use it locally and work on some features improvements
To run Portabase locally:
1. Clone the repo
```sh
git clone https://github.com/Soluce-Technologies/portabase
```
3. Start the project
```sh
docker compose up
```
1. Clone the repository:
```bash
git clone https://github.com/Soluce-Technologies/portabase
cd portabase
```
2. Start the development environment:
```bash
docker compose up
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
---
## 🛠️ Usage
<!-- ROADMAP -->
## Roadmap
Portabase provides a dashboard to manage database instances and backups. It supports:
- [ ] Add Changelog
- [ ] Improve documentation and repositories
- [ ] Add tests procedure
- [ ] Add Release File
- [ ] Security improvements
- [ ] Improve UX/UI
- [ ] Improve Workspace system
- [ ] Migrate to Drizzle ORM
- [ ] Multi locale support
- [ ] Multi db support
- PostgreSQL (current)
- MongoDB (coming soon)
- MySQL (coming soon)
You can access the dashboard at `http://localhost:3000` after starting the project.
---
## 🗺️ Roadmap
- [ ] Add changelog
- [ ] Enhance documentation
- [ ] Implement testing procedures
- [ ] Include release file
- [ ] Improve security
- [ ] Enhance UX/UI
- [ ] Improve workspace management system
- [x] Migrate to Drizzle ORM
- [ ] Add multi-language support
- [ ] Extend multi-database support:
- [x] PostgreSQL
- [ ] MongoDB
- [ ] MySQL
See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues).
Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
<!-- CONTRIBUTING -->
## Contributing
1. Fork the repository
2. Create a new branch:
```bash
git checkout -b feature/YourFeature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin feature/YourFeature
```
5. Open a pull request
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Give the project a ⭐ if you like it!
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!
### Top Contributors
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
[![Contributors](https://contrib.rocks/image?repo=Soluce-Technologies/portabase)](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
### Top contributors:
---
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
</a>
## Developer Notes
<p align="right">(<a href="#readme-top">back to top</a>)</p>
### Environment Variables
```yml
# Environment
NODE_ENV=production
## Memo
# Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format:
# Project Info
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)```
# SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)```
# Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
For zenstask
# S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
```zenstack generate```
# Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
```
<!-- LICENSE -->
## License
### Semantic Versioning
Distributed under the Apache License. See `LICENSE.txt` for more information.
Use the following format for Docker image versioning:
<p align="right">(<a href="#readme-top">back to top</a>)</p>
```bash
major.minor.patch-rc.release
# Example: 1.0.0-rc.1
major.minor.patch-rc.release-tag
# Example: 1.0.0-rc.1-dev
```
<!-- CONTACT -->
## Contact
### Zenstack Support
Killian Larcher - killian.larcher@soluce-technologies.com
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
To regenerate schema:
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase)
```bash
zenstack generate
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
---
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
## 📄 License
Distributed under the Apache License. See `LICENSE.txt` for more details.
---
## 📬 Contact
- Killian Larcher - killian.larcher@soluce-technologies.com
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
---
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[Prisma]: https://img.shields.io/badge/Prisma-3982CE?style=for-the-badge&logo=Prisma&logoColor=white
[Prisma-url]: https://www.prisma.io/
[NextJS-url]: https://nextjs.org/
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
[NextJS-url]: https://nextjs.org/
[Drizzle-url]: https://orm.drizzle.team/
[ShadcnUI-url]: https://ui.shadcn.com/
+16 -29
View File
@@ -1,42 +1,29 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs";
import { currentUser } from "@/lib/auth/current-user";
import { db } from "@/db";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser()!;
const users = await db.query.user.findMany({
where: (fields, { isNull, not }) => not(isNull(fields.deletedAt)),
});
const user = await requiredCurrentUser()
const users = await prisma.user.findMany({
where: {
// id: {
// not: user.id
// },
deleted: {not: true},
}
})
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
const settings = await db.query.setting.findFirst({
where: (fields, { eq }) => eq(fields.name, "system"),
});
return (
<Page>
<PageHeader>
<PageTitle>
Administration Panel
</PageTitle>
<PageTitle>Administration Panel</PageTitle>
</PageHeader>
<PageContent className="mt-10">
<AdminTabs settings={settings} currentUser={user} users={users}/>
<AdminTabs settings={settings!} currentUser={user} users={users} />
</PageContent>
</Page>
)
}
);
}
@@ -1,19 +1,18 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import { notFound } from "next/navigation";
import { db } from "@/db";
export default async function RoutePage(
props: PageParams<{
agentId: string;
}>
) {
const { agentId } = await props.params;
export default async function RoutePage(props: PageParams<{
agentId: string;
}>) {
const {agentId} = await props.params
const agent = await prisma.agent.findUnique({
where: {
id: agentId,
}
const agent = await db.query.agent.findFirst({
where: (fields, { eq }) => eq(fields.id, agentId),
});
if (!agent) {
@@ -23,13 +22,11 @@ export default async function RoutePage(props: PageParams<{
return (
<Page>
<PageHeader>
<PageTitle>
Edit {agent.name}
</PageTitle>
<PageTitle>Edit {agent.name}</PageTitle>
</PageHeader>
<PageContent>
<AgentForm defaultValues={agent} agentId={agent.id}/>
<AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent>
</Page>
)
}
);
}
@@ -1,28 +1,16 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import {currentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
if(user.role != "admin"){
notFound()
}
return (
<Page>
<PageHeader>
<PageTitle>
Create new agent
</PageTitle>
<PageTitle>Create new agent</PageTitle>
</PageHeader>
<PageContent>
<AgentForm/>
<AgentForm />
</PageContent>
</Page>
)
}
);
}
@@ -1,29 +1,24 @@
import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import Link from 'next/link'
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import { PageParams } from "@/types/next";
import { AgentCard } from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { db } from "@/db";
export const dynamic = "force-dynamic";
export default async function RoutePage(props: PageParams<{}>) {
const agents = await db.query.agent.findMany();
const agents = await prisma.agent.findMany()
if(!agents){
notFound()
if (!agents) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Agents
</PageTitle>
<PageTitle>Agents</PageTitle>
{agents.length > 0 && (
<PageActions>
<Link href={"/dashboard/agents/new"}>
@@ -33,21 +28,17 @@ export default async function RoutePage(props: PageParams<{}>) {
)}
</PageHeader>
<PageContent className="mt-10">
{agents.length > 0 ?
<CardsWithPagination
data={agents}
cardItem={AgentCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
{agents.length > 0 ? (
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
) : (
<Link
href={"/dashboard/agents/new"}
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
>
Create new Agent
</Link>
}
)}
</PageContent>
</Page>
)
}
);
}
+9 -13
View File
@@ -1,17 +1,13 @@
import React from "react";
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import { notFound } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user";
export default async function Layout({children}: { children: React.ReactNode }) {
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
const user = await requiredCurrentUser()
if(user.role != "admin"){
notFound()
if (user!.role !== "superadmin" && user!.role !== "admin") {
notFound();
}
return (
<>
{children}
</>
)
}
return <>{children}</>;
}
@@ -1,55 +1,41 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { RestoreForm } from "@/components/wrappers/dashboard/database/RestoreForm";
import { currentUser } from "@/lib/auth/current-user";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import { database, backup } from "@/db/schema";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
export default async function RoutePage(
props: PageParams<{
databaseId: string;
}>
) {
const { databaseId } = await props.params;
const {databaseId} = await props.params
const user = await currentUser();
if (!user) notFound();
const [dbToRestore] = await db.select().from(database).where(eq(database.id, databaseId));
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
const databases = await prisma.database.findMany({
where: {
dbms: database.dbms,
}
})
const backups = await prisma.backup.findMany({
where: {
status: "success",
}
});
if (!database) {
if (!dbToRestore) {
notFound();
}
const dbsOfSameType = await db.select().from(database).where(eq(database.dbms!, dbToRestore.dbms!));
const successfulBackups = await db.select().from(backup).where(eq(backup.status, "success"));
return (
<Page>
<PageHeader>
<PageTitle>
Restore {database.name}
</PageTitle>
<PageTitle>Restore {dbToRestore.name}</PageTitle>
</PageHeader>
<PageContent>
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/>
<RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups} />
</PageContent>
</Page>
)
}
);
}
+18 -15
View File
@@ -1,17 +1,20 @@
import {PageParams} from "@/types/next";
import {redirect} from "next/navigation";
"use client";
import { authClient } from "@/lib/auth/auth-client";
import { PageParams } from "@/types/next";
export default async function RoutePage(props: PageParams<{}>) {
export default function RoutePage(props: PageParams<{}>) {
const { data: activeOrganization } = authClient.useActiveOrganization();
// return (
// <div className="flex flex-1 flex-col gap-4 p-4">
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// </div>
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
// </div>
// )
}
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
{JSON.stringify(activeOrganization)}
{activeOrganization?.name}
</div>
);
}
@@ -1,39 +1,34 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {DatabaseForm} from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
import { db } from "@/db";
import { database } from "@/db/schema";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const { databaseId } = await props.params;
const {databaseId} = await props.params
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
const dbItem = await db.query.database.findFirst({
where: eq(database.id, databaseId),
});
if (!database) {
if (!dbItem) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Edit {database.name}
</PageTitle>
<PageTitle>Edit {dbItem.name}</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm databaseId={databaseId} defaultValues={database}/>
<DatabaseForm
databaseId={databaseId}
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
/>
</PageContent>
</Page>
)
}
);
}
@@ -1,91 +1,75 @@
import {PageParams} from "@/types/next";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
import { PageParams } from "@/types/next";
import { notFound } from "next/navigation";
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
import { BackupButton } from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import { DatabaseTabs } from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
import { DatabaseKpi } from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import { EditButton } from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import { CronButton } from "@/components/wrappers/dashboard/database/CronButton/CronButton";
import { db } from "@/db";
import { eq, and } from "drizzle-orm";
import { backup as drizzleBackup, database as drizzleDatabase, restoration as drizzleRestoration } from "@/db/schema";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const { databaseId } = await props.params;
const {databaseId} = await props.params
const database = await prisma.database.findUnique({
where: {
id: databaseId,
},
})
if (!database) {
notFound()
const dbItem = await db.query.database.findFirst({
where: eq(drizzleDatabase.id, databaseId),
});
if (!dbItem) {
notFound();
}
const backups = await prisma.backup.findMany({
where: {
databaseId: database.id
},
include:{
restorations: {}
},
orderBy: {
createdAt: "desc"
}
})
const restorations = await prisma.restoration.findMany({
where: {
databaseId: databaseId,
},
orderBy: {
createdAt: "desc"
}
})
const isAlreadyBackup = !!backups.find(backup => backup.status === "waiting");
const isAlreadyRestore = !!restorations.find(restoration => restoration.status === "waiting");
const totalBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
const backups = await db.query.backup.findMany({
where: eq(drizzleBackup.databaseId, dbItem.id),
with: {
restorations: true,
},
orderBy: (b, { desc }) => [desc(b.createdAt)],
});
const successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
const restorations = await db.query.restoration.findMany({
where: eq(drizzleRestoration.databaseId, dbItem.id),
orderBy: (r, { desc }) => [desc(r.createdAt)],
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const [totalBackups, successfulBackups] = await Promise.all([
db
.select({ count: drizzleBackup.id })
.from(drizzleBackup)
.where(eq(drizzleBackup.databaseId, dbItem.id))
.then((rows) => rows.length),
db
.select({ count: drizzleBackup.id })
.from(drizzleBackup)
.where(and(eq(drizzleBackup.databaseId, dbItem.id), eq(drizzleBackup.status, "success")))
.then((rows) => rows.length),
]);
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{database.name}
<EditButton/>
<CronButton database={database}/>
{dbItem.name}
<EditButton />
<CronButton database={dbItem} />
</PageTitle>
<PageActions className="justify-between">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
<BackupButton disable={isAlreadyBackup} databaseId={databaseId} />
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription>
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi
successRate={successRate}
database={database}
totalBackups={totalBackups}
/>
<DatabaseTabs
database={database}
isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}
/>
<DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups} />
<DatabaseTabs database={dbItem} isAlreadyRestore={isAlreadyRestore} backups={backups} restorations={restorations} />
</PageContent>
</Page>
)
}
);
}
@@ -1,61 +1,61 @@
import {notFound} from "next/navigation";
import { notFound } from "next/navigation";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import { project as drizzleProject, organization as drizzleOrganization, DatabaseWith } from "@/db/schema";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const { projectId } = await props.params;
export default async function RoutePage(props: PageParams<{
projectId: string;
}>) {
const {projectId} = await props.params
const project = await prisma.project.findUnique({
where: {
id: projectId,
const proj = await db.query.project.findFirst({
where: eq(drizzleProject.id, projectId),
with: {
databases: true,
},
include: {
databases: {}
}
});
if (!project) {
if (!proj) {
notFound();
}
const organization = await prisma.organization.findFirst({
where: {
slug: 'default',
}
})
const availableDatabases = await prisma.database.findMany({
where: {
OR: [
{projectId: null},
{projectId: project.id},
],
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
//
const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, "default"),
});
if (!org) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, { or, eq, isNull }) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, { desc }) => [desc(db.createdAt)],
})
).filter((db): db is DatabaseWith => db.project !== null);
return (
<Page>
<PageHeader>
<PageTitle>
Edit {project.name}
</PageTitle>
<PageTitle>Edit {proj.name}</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm organization={organization} databases={availableDatabases} defaultValues={project}
projectId={project.id}/>
<ProjectForm
organization={org}
databases={availableDatabases}
defaultValues={{ name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id) }}
projectId={proj.id}
/>
</PageContent>
</Page>
)
}
);
}
@@ -1,77 +1,70 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button";
import {prisma} from "@/prisma";
import {GearIcon} from "@radix-ui/react-icons";
import { PageParams } from "@/types/next";
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
import { buttonVariants } from "@/components/ui/button";
import { GearIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import {notFound} from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import { notFound } from "next/navigation";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import { organization as drizzleOrganization } from "@/db/schema";
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) {
export default async function RoutePage(props: PageParams<{ slug: string; projectId: string }>) {
const { slug: organizationSlug, projectId } = await props.params;
const {slug: organizationSlug, projectId} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug();
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
if (currentOrganizationSlug !== organizationSlug) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
});
if (!org) notFound();
const project = await prisma.project.findUnique({
where: {
id: projectId,
isArchived: {not: true}
const proj = await db.query.project.findFirst({
where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
with: {
databases: true,
},
include:{
databases: {}
}
})
});
if(!project){
notFound()
}
if (!proj) notFound();
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{project.name}
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
<GearIcon className="w-7 h-7"/>
{proj.name}
<Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/${currentOrganizationSlug}/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7" />
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteProject
projectId={projectId}
text={"Delete Project"}
/>
<ButtonDeleteProject projectId={projectId} text={"Delete Project"} />
</PageActions>
</div>
<PageDescription>
The list of associated databases
</PageDescription>
<PageDescription>The list of associated databases</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{project.databases.length > 0 ?
{proj.databases.length > 0 ? (
<CardsWithPagination
data={project.databases}
data={proj.databases}
organizationSlug={organizationSlug}
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={project}
extendedProps={proj}
/>
:
null
}
) : null}
</PageContent>
</Page>
)
}
);
}
@@ -1,48 +1,48 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {notFound} from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import { notFound } from "next/navigation";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { db } from "@/db";
import { DatabaseWith, organization as drizzleOrganization } from "@/db/schema";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const { slug: organizationSlug } = await props.params;
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
const currentOrganizationSlug = await getCurrentOrganizationSlug();
if (currentOrganizationSlug !== organizationSlug) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, { isNull }) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, { desc }) => [desc(db.createdAt)],
})
).filter((db) => db.project !== null) as DatabaseWith[];
const availableDatabases = await prisma.database.findMany({
where: {
projectId: null
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
});
const organization = await prisma.organization.findFirst({
where: {
slug: currentOrganizationSlug,
}
})
if (!org) notFound();
return (
<Page>
<PageHeader>
<PageTitle>
Create new project
</PageTitle>
<PageTitle>Create new project</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={organization}/>
<ProjectForm databases={availableDatabases} organization={org} />
</PageContent>
</Page>
)
}
);
}
@@ -1,43 +1,34 @@
import Link from 'next/link'
import Link from "next/link";
import {PageParams} from "@/types/next";
import {prisma} from "@/prisma";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {notFound} from "next/navigation";
import { PageParams } from "@/types/next";
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import { Button } from "@/components/ui/button";
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { ProjectCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
import { db } from "@/db";
import { notFound } from "next/navigation";
import { getOrganization } from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const { slug: organizationSlug } = await props.params;
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const organization = await getOrganization(organizationSlug);
if (!organization || organization?.slug !== organizationSlug) {
notFound();
}
const projects = await prisma.project.findMany({
where: {
isArchived: {not: true},
organization: {
slug: organizationSlug,
// users: {
// some: {
// userId: user.id
// },
// },
}
const projects = await db.query.project.findMany({
where: (project, { eq, and, not }) => and(eq(project.organizationId, organization.id), not(eq(project.isArchived, true))),
with: {
databases: true,
},
include: {
databases: {}
}
})
});
return (
<Page>
<PageHeader>
<PageTitle>
Projects
</PageTitle>
<PageTitle>Projects</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/${organizationSlug}/projects/new`}>
@@ -45,28 +36,20 @@ export default async function RoutePage(props: PageParams<{slug: string}>) {
</Link>
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{projects.length > 0 ?
<CardsWithPagination
organizationSlug={organizationSlug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
{projects.length > 0 ? (
<CardsWithPagination organizationSlug={organizationSlug} data={projects} cardItem={ProjectCard} cardsPerPage={4} numberOfColumns={1} />
) : (
<Link
href={`/dashboard/${organizationSlug}/projects/new`}
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
>
Create new Project
</Link>
}
)}
</PageContent>
</Page>
)
}
);
}
@@ -1,48 +1,57 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { notFound } from "next/navigation";
import { db } from "@/db";
import { asc, count, eq, inArray } from "drizzle-orm";
import { organization as drizzleOrganization, project as drizzleProject, backup as drizzleBackup } from "@/db/schema";
export default async function RoutePage(props: PageParams<{}>) {
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const { slug: organizationSlug } = await props.params;
const projectsCount = await prisma.project.count({
where: {
organization: {
slug: "default"
}
},
const currentOrganizationSlug = await getCurrentOrganizationSlug();
if (currentOrganizationSlug !== organizationSlug) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
});
const backupsEvolution = await prisma.backup.findMany({
select: {
if (!org) notFound();
const projects = await db.query.project.findMany({
where: eq(drizzleProject.organizationId, org.id),
});
const projectsCount = projects.length;
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
},
orderBy: {
createdAt: "asc",
},
});
const backupsRate = await prisma.backup.groupBy({
by: ['createdAt', 'status'],
where: {
status: {
in: ['success', 'failed'],
},
},
_count: {
id: true,
},
orderBy: [asc(drizzleBackup.id)],
});
const backupsRate = await db
.select({
createdAt: drizzleBackup.createdAt,
status: drizzleBackup.status,
_count: count(),
})
.from(drizzleBackup)
.where(inArray(drizzleBackup.status, ["success", "failed"]))
.groupBy(drizzleBackup.createdAt, drizzleBackup.status)
.orderBy(drizzleBackup.createdAt);
return (
<Page>
<PageHeader>
<PageTitle>
Statistics
</PageTitle>
<PageTitle>Statistics</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
@@ -50,23 +59,19 @@ export default async function RoutePage(props: PageParams<{}>) {
<CardHeader>
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent>
{projectsCount}
</CardContent>
<CardContent>{projectsCount}</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 2</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
<CardContent></CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 3</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
<CardContent></CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
@@ -75,7 +80,7 @@ export default async function RoutePage(props: PageParams<{}>) {
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={backupsEvolution}/>
<EvolutionLineChart data={backupsEvolution} />
</CardContent>
</Card>
<Card className="w-full">
@@ -83,11 +88,11 @@ export default async function RoutePage(props: PageParams<{}>) {
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate}/>
<PercentageLineChart data={backupsRate} />
</CardContent>
</Card>
</div>
</PageContent>
</Page>
)
}
);
}
+13 -19
View File
@@ -1,37 +1,31 @@
import React from "react";
import {redirect} from "next/navigation";
import { redirect } from "next/navigation";
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar"
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar";
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
import {Header} from "@/features/layout/Header";
import {prisma} from "@/prisma";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/wrappers/dashboard/sideBar/app-sidebar";
import { Header } from "@/features/layout/Header";
import { currentUser } from "@/lib/auth/current-user";
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await requiredCurrentUser()
if (!user) redirect('/login')
if (!user) redirect("/login");
return (
// <HydrationZustand>
// <GlobalStoreProvider>
<>
<SidebarProvider>
<div className="flex flex-col lg:flex-row w-full">
<AppSidebar/>
<AppSidebar />
<SidebarInset>
<Header/>
<main className="h-full">
{children}
</main>
<Header />
<main className="h-full">{children}</main>
</SidebarInset>
</div>
</SidebarProvider>
</>
// </GlobalStoreProvider>
// </HydrationZustand>
)
}
);
}
+3 -3
View File
@@ -1,9 +1,9 @@
import {LoadingSpinner} from "@/components/wrappers/common/loading/LoadingSpinner";
import { LoadingSpinner } from "@/components/wrappers/common/loading/LoadingSpinner";
export default function Loading() {
return (
<div className="w-full h-full flex flex-col items-center justify-center">
<LoadingSpinner size={50} />
</div>
)
}
);
}
+49 -31
View File
@@ -1,44 +1,62 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {requiredCurrentUser} from "@/auth/current-user";
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
import {prisma} from "@/prisma";
import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
import { PageParams } from "@/types/next";
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { UserForm } from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
import { Badge } from "@/components/ui/badge";
import { ButtonDeleteAccount } from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
import { AvatarWithUpload } from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
import { currentUser } from "@/lib/auth/current-user";
//import { getAccounts, getSessions } from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
const user = await requiredCurrentUser()
if (!user) {
notFound()
return notFound();
}
const userInfo = await prisma.user.findUnique({
where: {
email: user.email
}
})
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
return notFound();
}
// const sessions = await getSessions();
// const accounts = await getAccounts();
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
<AvatarWithUpload user={userInfo}/>
{user.name}
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge>
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge>
</PageTitle>
<PageActions className="mt-2 hidden sm:block">
<ButtonDeleteAccount text="Delete my account"/>
</PageActions>
</div>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
<AvatarWithUpload
user={{
...user,
image: user.image ?? null,
role: user.role ?? null,
banned: user.banned ?? null,
banReason: user.banReason ?? null,
banExpires: user.banExpires ?? null,
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
}}
/>
{user.name}
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
</PageTitle>
<PageActions className="mt-2 hidden sm:block">
<ButtonDeleteAccount text="Delete my account" />
</PageActions>
</div>
<PageContent>
<UserForm userId={userInfo.id} defaultValues={userInfo}/>
<UserForm
userId={user.id}
defaultValues={{
name: user.name,
email: user.email,
role: user.role ?? undefined,
}}
/>
<div className="mt-4 sm:hidden">
<ButtonDeleteAccount text="Delete my account" />
</div>
</PageContent>
</Page>
)
}
);
}
+5 -5
View File
@@ -1,7 +1,7 @@
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-3xl font-bold text-4xl leading-tight">Home landing page</h1>
</div>
);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="font-bold text-4xl leading-tight">Home landing page</h1>
</div>
);
}
+7 -8
View File
@@ -1,15 +1,14 @@
import {redirect} from "next/navigation";
import {currentUser} from "@/auth/current-user";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import { redirect } from "next/navigation";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { currentUser } from "@/lib/auth/current-user";
export default async function Index() {
const user = await currentUser()
const user = await currentUser();
if (user) {
const currentOrganizationSlug = await getCurrentOrganizationSlug()
redirect(`/dashboard/${currentOrganizationSlug}/projects`)
const currentOrganizationSlug = await getCurrentOrganizationSlug();
redirect(`/dashboard/${currentOrganizationSlug}/projects`);
}
redirect("/login")
redirect("/login");
//Do not delete
// return (
// <Home/>
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
-4
View File
@@ -1,4 +0,0 @@
import {handlers} from "@/auth/auth";
export const {GET, POST} = handlers;
+5 -9
View File
@@ -1,21 +1,17 @@
import path from "path";
import fs from "fs/promises";
import {NextResponse} from "next/server";
export async function GET(
request: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
import { NextResponse } from "next/server";
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
try {
const fileName = (await params).fileName
const fileName = (await params).fileName;
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
// Check if the file exists
try {
await fs.access(filePath); // Ensures the file exists
} catch {
return NextResponse.json({error: "File not found"}, {status: 404});
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
// Read the file
@@ -29,6 +25,6 @@ export async function GET(
});
} catch (error) {
console.error("Error reading file:", error);
return NextResponse.json({error: "Internal Server Error"}, {status: 500});
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}
+7 -16
View File
@@ -1,20 +1,14 @@
import {NextResponse} from "next/server";
import { NextResponse } from "next/server";
export type BodyInit = {
initialize: boolean
}
initialize: boolean;
};
export async function POST(
request: Request,
) {
export async function POST(request: Request) {
try {
const body: BodyInit = await request.json();
console.log(body)
console.log(body);
return NextResponse.json(
{
@@ -24,9 +18,6 @@ export async function POST(
);
} catch (error) {
console.error("Error in POST initialization:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
}
+2 -2
View File
@@ -24,7 +24,7 @@
// <div className="flex h-screen w-full flex-col items-center justify-center">
// <a
// href="#"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// >
// <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
// Something went wrong
@@ -63,7 +63,7 @@ function AuthErrorPageContent() {
<div className="flex h-screen w-full flex-col items-center justify-center">
<a
href="#"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
>
<h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Something went wrong
+127 -90
View File
@@ -1,105 +1,142 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@import "tw-animate-css";
/*body {*/
/* font-family: Arial, Helvetica, sans-serif;*/
/*}*/
@custom-variant dark (&:is(.dark *));
@layer utilities {
/*.text-balance {*/
/* text-wrap: balance;*/
/*}*/
.popover-content-width-full {
width: var(--radix-popover-trigger-width);
max-height: var(--radix-popover-content-available-height);
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border;
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+10 -15
View File
@@ -1,13 +1,12 @@
import React from "react";
import type {Metadata} from "next";
import {Inter} from "next/font/google";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import {Providers} from "./providers";
import {cn} from "@/lib/utils";
import { Providers } from "./providers";
import { cn } from "@/lib/utils";
const inter = Inter({subsets: ["latin"]});
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Portabase",
@@ -15,19 +14,15 @@ export const metadata: Metadata = {
};
export default function RootLayout({
children,
}: Readonly<{
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(inter.className, "h-full")}
>
<Providers>
{children}
</Providers>
</body>
<body className={cn(inter.className, "h-full")}>
<Providers>{children}</Providers>
</body>
</html>
);
}
+3 -4
View File
@@ -1,6 +1,5 @@
import {redirect, useRouter} from "next/navigation";
import { redirect } from "next/navigation";
export default async function NotFound() {
console.log("not found");
redirect("/")
}
redirect("/dashboard/profile");
}
+14 -23
View File
@@ -1,32 +1,23 @@
"use client"
"use client";
import {PropsWithChildren, Suspense} from "react";
import {ThemeProvider} from "@/features/theme/theme-provider";
import { PropsWithChildren, Suspense } from "react";
import { ThemeProvider } from "@/features/theme/theme-provider";
import {Toaster} from "@/components/ui/sonner";
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query'
import {SessionProvider} from "next-auth/react";
import { Toaster } from "@/components/ui/sonner";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient()
const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => {
return (
<Suspense fallback={null}>
<SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
// disableTransitionOnChange
>
<QueryClientProvider client={queryClient}>
<Toaster/>
{props.children}
</QueryClientProvider>
</ThemeProvider>
</SessionProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
<Toaster />
{props.children}
</QueryClientProvider>
</ThemeProvider>
</Suspense>
)
}
);
};
+68 -59
View File
@@ -1,68 +1,77 @@
name: portabase-dev
services:
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: dev
ports:
- "8887:80"
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: dev
ports:
- '8887:80'
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s
timeout: 5s
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
develop:
watch:
- action: sync
path: .
target: /app
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s
timeout: 5s
retries: 5
db-pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
PGADMIN_DEFAULT_PASSWORD: "changeme"
PGADMIN_CONFIG_SERVER_MODE: "False"
POSTGRES_USER: "devuser"
POSTGRES_PASSWORD: "changeme"
volumes:
- pgadmin-data:/var/lib/pgadmin
ports:
- "8080:80"
restart: unless-stopped
depends_on:
- db
s3:
image: docker.io/bitnami/minio:latest
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
volumes:
postgres-data:
minio_data:
postgres-data:
minio_data:
pgadmin-data:
+1 -4
View File
@@ -3,11 +3,8 @@
npm install -g npm@11.0.0
npm -v
current_date=$(date +"%Y-%m-%d")
npx zenstack generate
npx prisma migrate dev --name $current_date
npx drizzle-kit migrate
npm run dev
exec "$@"
+1 -1
View File
@@ -10,7 +10,7 @@ echo " Community Edition v1.0.0 "
echo " "
npx prisma migrate deploy
npx drizzle-kit migrate
node server.js
exec "$@"
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({
path: ".env",
});
export default defineConfig({
out: "./src/db/migrations",
schema: ["./src/db/schema", "./src/db/schema/types.ts"],
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
+4 -11
View File
@@ -1,13 +1,6 @@
import {
Button as ReactEmailButton
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
import { Button as ReactEmailButton } from "@react-email/components";
import { ComponentPropsWithoutRef } from "react";
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
return (
<ReactEmailButton
className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline "
{...props}
/>
)
}
return <ReactEmailButton className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline " {...props} />;
};
+5 -27
View File
@@ -1,18 +1,7 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from "@react-email/components";
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components";
import * as React from "react";
import { PropsWithChildren } from "react";
import {getServerUrl} from "@/utils/get-server-url";
import { getServerUrl } from "@/utils/get-server-url";
const baseUrl = getServerUrl();
@@ -22,19 +11,10 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
<Html>
<Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4"
style={{fontFamily: "Arial, sans-serif"}}>
<Body className="bg-gray-100 py-4" style={{ fontFamily: "Arial, sans-serif" }}>
<Container className="bg-white border border-gray-200 p-12">
<Img
src={`${baseUrl}/logo-title-black.png`}
width="200"
height="auto"
alt="Logo"
/>
<Section>
{children}
</Section>
<Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
<Section>{children}</Section>
</Container>
</Body>
</Html>
@@ -42,6 +22,4 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
);
};
export default EmailLayout;
+3 -6
View File
@@ -5,12 +5,9 @@ import * as React from "react";
export default function HelloEmail() {
return (
<Html>
<Button
href="https://example.com"
style={{ background: "#000", color: "#fff" }}
>
Hello Charles
<Button href="https://example.com" style={{ background: "#000", color: "#fff" }}>
Hello,
</Button>
</Html>
);
}
}
+6 -32
View File
@@ -1,42 +1,16 @@
import {
Text,
} from "@react-email/components";
import { Text } from "@react-email/components";
import * as React from "react";
import EmailLayout from "./EmailLayout";
import {Button} from "@react-email/button";
import { Button } from "@react-email/button";
export const TestEmailSettings = () => {
return (
<EmailLayout
preview="Email Setup"
>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Hi, your email settings are setup !
</Text>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Best regard,
</Text> <Text
className="text-base font-light leading-8 text-green-800 "
>
Portabase
</Text>
<EmailLayout preview="Email Setup">
<Text className="text-base font-light leading-8 text-green-800 ">Hi, your email settings are setup !</Text>
<Text className="text-base font-light leading-8 text-green-800 ">Best regard,</Text>{" "}
<Text className="text-base font-light leading-8 text-green-800 ">Portabase</Text>
</EmailLayout>
);
};
export default TestEmailSettings;
+4 -11
View File
@@ -1,13 +1,6 @@
import {
Text as ReactEmailText
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
import { Text as ReactEmailText } from "@react-email/components";
import { ComponentPropsWithoutRef } from "react";
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
return (
<ReactEmailText
className="text-base font-light leading-8 text-green-800 "
{...props}
/>
)
}
return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
};
+5 -5
View File
@@ -1,6 +1,6 @@
import {init} from "@/utils/init";
export function register() {
init();
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const init = await import("@/utils/init");
init.init();
}
}
+35 -22
View File
@@ -1,43 +1,54 @@
import {NextRequest, NextResponse} from 'next/server'
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
import {errorHandler} from "@/middleware/errorHandler";
import { NextRequest, NextResponse } from "next/server";
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import { errorHandler } from "@/middleware/errorHandler";
import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers";
export function middleware(request: NextRequest) {
export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
// Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith('/api/auth')) {
if (url.pathname.startsWith("/dashboard")) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
if (url.pathname.startsWith('/api')) {
// Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname);
// If the route does not exist, return a 404 JSON response
if (!routeExists) {
return new NextResponse(
JSON.stringify({ message: "This API route does not exist.", status: 404 }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
}
try{
try {
loggingMiddleware(request);
}catch(err){
errorHandler(err)
} catch (err) {
errorHandler(err);
}
}
// Function to check if the route exists (supports dynamic routes)
function checkRouteExists(pathname) {
function checkRouteExists(pathname: string) {
// Define static and dynamic routes with patterns
const routePatterns = [
//do not delete
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
/^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/[^/]+\/?$/,
@@ -45,13 +56,15 @@ function checkRouteExists(pathname) {
/^\/api\/events\/?$/,
/^\/api\/init\/?$/,
];
return routePatterns.some(pattern => pattern.test(pathname));
return routePatterns.some((pattern) => pattern.test(pathname));
}
export const config = {
runtime: "nodejs",
matcher: [
// '/api/agent/:path*',
'/api/:path*',
"/api/:path*",
"/dashboard/:path*",
"/dashboard",
],
};
};
+6 -7
View File
@@ -1,6 +1,6 @@
import type { NextConfig } from "next";
/** @type {import('next').NextConfig} */
const nextConfig = {
const nextConfig: NextConfig = {
output: "standalone",
typescript: {
ignoreBuildErrors: true,
@@ -9,12 +9,11 @@ const nextConfig = {
ignoreDuringBuilds: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
removeConsole: process.env.NODE_ENV === "production",
},
experimental: {
nodeMiddleware: true,
},
};
export default nextConfig;
+112 -104
View File
@@ -1,106 +1,114 @@
{
"name": "portabase",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 80",
"build": "next build",
"start": "next start",
"lint": "next lint",
"email": "email dev"
},
"dependencies": {
"@auth/prisma-adapter": "^2.7.4",
"@hookform/resolvers": "^3.9.1",
"@prisma/client": "^6.3.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-aspect-ratio": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-context-menu": "^2.2.5",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-hover-card": "^1.1.5",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-menubar": "^1.1.5",
"@radix-ui/react-navigation-menu": "^1.2.4",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.7",
"@react-email/components": "^0.0.32",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-table": "^8.20.6",
"@types/ws": "^8.5.13",
"@zenstackhq/runtime": "2.10.2",
"argon2": "^0.41.1",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
"dockerode": "^4.0.2",
"embla-carousel-react": "^8.3.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.474.0",
"minio": "^8.0.2",
"next": "15.1.7",
"next-auth": "^5.0.0-beta.25",
"next-safe-action": "^7.10.2",
"next-themes": "^0.4.4",
"nodemailer": "^6.10.0",
"npm-check-updates": "^17.1.13",
"react": "19.0.0",
"react-day-picker": "^9.5.1",
"react-dom": "19.0.0",
"react-email": "^3.0.6",
"react-hook-form": "^7.54.2",
"react-resizable-panels": "^2.1.7",
"react-twc": "^1.4.2",
"recharts": "^2.15.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"sonner": "^1.7.3",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.5",
"vaul": "^1.1.2",
"ws": "^8.18.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/eslint-plugin-tailwindcss": "^3",
"@types/node": "^22",
"@types/pg": "^8",
"@types/react": "^19",
"@types/react-dom": "^19",
"@zenstackhq/openapi": "^2.10.2",
"@zenstackhq/tanstack-query": "^2.10.2",
"eslint": "^8",
"eslint-config-next": "15.1.6",
"eslint-plugin-tailwindcss": "^3.17.5",
"postcss": "^8",
"prisma": "^6.3.0",
"tailwindcss": "^3.4.1",
"typescript": "^5",
"zenstack": "2.10.2"
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
"name": "portabase",
"version": "1.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 80",
"build": "next build",
"start": "next start",
"lint": "next lint",
"email": "email dev",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:drop": "drizzle-kit drop",
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-aspect-ratio": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-context-menu": "^2.2.14",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-menubar": "^1.1.14",
"@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-progress": "^1.1.6",
"@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slider": "^1.3.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-toggle": "^1.1.8",
"@radix-ui/react-toggle-group": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.6",
"@react-email/components": "^0.0.41",
"@t3-oss/env-nextjs": "^0.13.4",
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-table": "^8.21.3",
"@types/nodemailer": "^6.4.17",
"@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.0",
"bcrypt": "^6.0.0",
"better-auth": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dockerode": "^4.0.6",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.510.0",
"minio": "^8.0.5",
"next": "^15.4.0-canary.36",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.3",
"npm-check-updates": "^18.0.1",
"pg": "^8.16.0",
"react": "19.1.0",
"react-day-picker": "9.7.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-email": "^4.0.13",
"react-hook-form": "^7.56.3",
"react-resizable-panels": "^3.0.2",
"react-twc": "^1.4.2",
"recharts": "^2.15.3",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"ws": "^8.18.2",
"zod": "^3.24.4"
},
"devDependencies": {
"@iconify/react": "^6.0.0",
"@tailwindcss/postcss": "^4.1.7",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.15.18",
"@types/pg": "^8.15.2",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@zenstackhq/openapi": "^2.14.2",
"@zenstackhq/tanstack-query": "^2.14.2",
"drizzle-kit": "^0.31.1",
"eslint": "^9.26.0",
"eslint-config-next": "15.3.2",
"eslint-plugin-tailwindcss": "^3.18.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.7",
"tsx": "^4.19.4",
"tw-animate-css": "^1.2.9",
"typescript": "^5.8.3",
"zenstack": "2.14.2"
},
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6"
}
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
},
};
@@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"session_token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verificationtokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
"role" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
-- CreateIndex
CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "password" TEXT;
@@ -1,46 +0,0 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('waiting', 'ongoing', 'failed', 'success');
-- CreateTable
CREATE TABLE "Agent" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Database" (
"id" TEXT NOT NULL,
"agent_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Database_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Backup" (
"id" TEXT NOT NULL,
"database_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Backup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Restore" (
"id" TEXT NOT NULL,
"backup_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Restore_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "authMethod" TEXT;
@@ -1,15 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Agent` will be added. If there are existing duplicate values, this will fail.
- Added the required column `slug` to the `Agent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Agent" ADD COLUMN "slug" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Agent_slug_key" ON "Agent"("slug");
-- AddForeignKey
ALTER TABLE "Database" ADD CONSTRAINT "Database_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,33 +0,0 @@
/*
Warnings:
- You are about to drop the column `authMethod` on the `users` table. All the data in the column will be lost.
- You are about to drop the `Restore` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "authMethod",
ADD COLUMN "auth_method" TEXT;
-- DropTable
DROP TABLE "Restore";
-- CreateTable
CREATE TABLE "Restauration" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "Restauration_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Backup" ADD CONSTRAINT "Backup_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "Backup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,13 +0,0 @@
-- CreateEnum
CREATE TYPE "TypeStorage" AS ENUM ('local', 's3');
-- CreateTable
CREATE TABLE "Settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
CONSTRAINT "Settings_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "s3EndPointUrl" TEXT;
@@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "smtpFrom" TEXT,
ADD COLUMN "smtpHost" TEXT,
ADD COLUMN "smtpPassword" TEXT,
ADD COLUMN "smtpPort" TEXT,
ADD COLUMN "smtpUser" TEXT;
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `name` to the `Settings` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "name" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Settings` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Settings_name_key" ON "Settings"("name");
@@ -1,118 +0,0 @@
/*
Warnings:
- You are about to drop the `Agent` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Backup` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Database` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Restauration` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Settings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Backup" DROP CONSTRAINT "Backup_database_id_fkey";
-- DropForeignKey
ALTER TABLE "Database" DROP CONSTRAINT "Database_agent_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_backup_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_database_id_fkey";
-- DropTable
DROP TABLE "Agent";
-- DropTable
DROP TABLE "Backup";
-- DropTable
DROP TABLE "Database";
-- DropTable
DROP TABLE "Restauration";
-- DropTable
DROP TABLE "Settings";
-- CreateTable
CREATE TABLE "agents" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "databases" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"agent_id" TEXT NOT NULL,
CONSTRAINT "databases_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "backups" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"database_id" TEXT NOT NULL,
CONSTRAINT "backups_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "restaurations" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "restaurations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"name" TEXT NOT NULL,
"s3EndPointUrl" TEXT,
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
"smtpPassword" TEXT,
"smtpFrom" TEXT,
"smtpHost" TEXT,
"smtpPort" TEXT,
"smtpUser" TEXT,
CONSTRAINT "settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "agents_slug_key" ON "agents"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "settings_name_key" ON "settings"("name");
-- AddForeignKey
ALTER TABLE "databases" ADD CONSTRAINT "databases_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "backups" ADD CONSTRAINT "backups_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,11 +0,0 @@
/*
Warnings:
- Added the required column `dbms` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Dbms" AS ENUM ('postgresql', 'mysql', 'mongodb');
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "dbms" "Dbms" NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "deleted" BOOLEAN;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "deleted" SET DEFAULT false;
@@ -1,12 +0,0 @@
/*
Warnings:
- Added the required column `role` to the `users` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('pending', 'user', 'admin');
-- AlterTable
ALTER TABLE "users" DROP COLUMN "role",
ADD COLUMN "role" "Role" NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "last_contact" TIMESTAMP(3);
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `generatedId` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "generatedId" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[generatedId]` on the table `databases` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "databases_generatedId_key" ON "databases"("generatedId");
@@ -1,9 +0,0 @@
/*
Warnings:
- Added the required column `backupToRestore` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "backupToRestore" TEXT NOT NULL,
ADD COLUMN "isWaitingForBackup" BOOLEAN NOT NULL DEFAULT false;
@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ALTER COLUMN "backupToRestore" DROP NOT NULL,
ALTER COLUMN "isWaitingForBackup" DROP NOT NULL;
@@ -1,35 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "project_id" TEXT;
-- CreateTable
CREATE TABLE "organizations" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"organization_id" TEXT NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "projects_slug_key" ON "projects"("slug");
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "databases" ADD CONSTRAINT "databases_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "_OrganizationToUser" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_OrganizationToUser_AB_unique" ON "_OrganizationToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_OrganizationToUser_B_index" ON "_OrganizationToUser"("B");
-- AddForeignKey
ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `_OrganizationToUser` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_A_fkey";
-- DropForeignKey
ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_B_fkey";
-- DropTable
DROP TABLE "_OrganizationToUser";
-- CreateTable
CREATE TABLE "UserOrganization" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
CONSTRAINT "UserOrganization_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "UserOrganization_userId_organizationId_key" ON "UserOrganization"("userId", "organizationId");
-- AddForeignKey
ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `UserOrganization` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_organizationId_fkey";
-- DropForeignKey
ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_userId_fkey";
-- DropTable
DROP TABLE "UserOrganization";
-- CreateTable
CREATE TABLE "users_organisations" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
CONSTRAINT "users_organisations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_organisations_userId_organizationId_key" ON "users_organisations"("userId", "organizationId");
-- AddForeignKey
ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,31 +0,0 @@
/*
Warnings:
- You are about to drop the `restaurations` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "restaurations" DROP CONSTRAINT "restaurations_backup_id_fkey";
-- DropForeignKey
ALTER TABLE "restaurations" DROP CONSTRAINT "restaurations_database_id_fkey";
-- DropTable
DROP TABLE "restaurations";
-- CreateTable
CREATE TABLE "restorations" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "restorations_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,45 +0,0 @@
/*
Warnings:
- The required column `id` was added to the `verificationtokens` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
*/
-- AlterTable
ALTER TABLE "accounts" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "agents" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "backups" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "organizations" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "restorations" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "sessions" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "settings" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "users_organisations" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "verificationtokens" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "id" TEXT NOT NULL,
ADD COLUMN "updated_at" TIMESTAMP(3),
ADD CONSTRAINT "verificationtokens_pkey" PRIMARY KEY ("id");
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `isArchived` to the `projects` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "isArchived" BOOLEAN NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "projects" ALTER COLUMN "isArchived" SET DEFAULT false;
@@ -1,9 +0,0 @@
/*
Warnings:
- You are about to drop the column `isArchived` on the `projects` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "projects" DROP COLUMN "isArchived",
ADD COLUMN "is_archived" BOOLEAN NOT NULL DEFAULT false;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "organizations" ADD COLUMN "deleted" BOOLEAN DEFAULT false;
@@ -1,11 +0,0 @@
/*
Warnings:
- Added the required column `role` to the `users_organisations` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "OrganizationRole" AS ENUM ('member', 'admin');
-- AlterTable
ALTER TABLE "users_organisations" ADD COLUMN "role" "OrganizationRole" NOT NULL;
-3
View File
@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
-228
View File
@@ -1,228 +0,0 @@
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
}
enum Role {
pending
user
admin
}
enum OrganizationRole {
member
admin
}
enum Dbms {
postgresql
mysql
mongodb
}
enum Status {
waiting
ongoing
failed
success
}
enum TypeStorage {
local
s3
}
model Account {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text()
access_token String? @db.Text()
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text()
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
sessionToken String @unique() @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verificationtokens")
}
model User {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
name String?
email String? @unique()
emailVerified DateTime? @map("email_verified")
image String?
role Role
password String?
authMethod String? @map("auth_method")
deleted Boolean? @default(false)
accounts Account[]
sessions Session[]
organizations UserOrganization[]
@@map("users")
}
model Organization {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
slug String @unique()
name String
projects Project[]
users UserOrganization[]
deleted Boolean? @default(false)
@@map("organizations")
}
model UserOrganization {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
userId String
organizationId String
user User @relation(fields: [userId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
role OrganizationRole
@@unique([userId, organizationId])
@@map("users_organisations")
}
model Project {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
slug String @unique()
name String
isArchived Boolean @default(false) @map("is_archived")
organizationId String @map("organization_id")
organization Organization @relation(fields: [organizationId], references: [id])
databases Database[]
@@map("projects")
}
model Agent {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
slug String @unique()
name String
description String?
lastContact DateTime? @map("last_contact")
databases Database[]
@@map("agents")
}
model Database {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
name String
dbms Dbms
generatedId String @unique()
description String?
backupPolicy String? @map("backup_policy")
isWaitingForBackup Boolean? @default(false)
backupToRestore String?
agentId String @map("agent_id")
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
lastContact DateTime? @map("last_contact")
backups Backup[]
restorations Restoration[]
projectId String? @map("project_id")
project Project? @relation(fields: [projectId], references: [id])
@@map("databases")
}
model Backup {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
status Status @default(waiting)
file String?
databaseId String @map("database_id")
database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
restorations Restoration[]
@@map("backups")
}
model Restoration {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
status Status @default(waiting)
backupId String @map("backup_id")
backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade)
databaseId String? @map("database_id")
database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade)
@@map("restorations")
}
model Settings {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
storage TypeStorage @default(local)
name String @unique()
s3EndPointUrl String?
s3AccessKeyId String?
s3SecretAccessKey String?
S3BucketName String?
smtpPassword String?
smtpFrom String?
smtpHost String?
smtpPort String?
smtpUser String?
@@map("settings")
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

-240
View File
@@ -1,240 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
// https://zenstack.dev/docs/guides/check-permission
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
plugin openapi {
provider = "@zenstackhq/openapi"
output = "./src/openapi.yaml"
}
plugin tanstack_query {
provider = "@zenstackhq/tanstack-query"
output = "./src/lib/hooks"
target = "react"
}
plugin enhancer {
provider = '@core/enhancer'
generatePermissionChecker = true
}
abstract model Base {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
}
model Account extends Base {
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session extends Base {
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken extends Base {
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verificationtokens")
}
model User extends Base {
name String?
email String? @unique @email
emailVerified DateTime? @map("email_verified")
image String?
role Role
password String? @omit @password
authMethod String? @map("auth_method")
deleted Boolean? @default(false)
accounts Account[]
sessions Session[]
organizations UserOrganization[]
@@map("users")
}
enum Role {
pending
user
admin
}
model Organization extends Base {
slug String @unique
name String
projects Project[]
users UserOrganization[]
deleted Boolean? @default(false)
// Allow only users who are part of the organization
@@allow('all', users?[user == auth()])
@@map("organizations")
}
enum OrganizationRole {
member
admin
}
model UserOrganization extends Base {
userId String
organizationId String
user User @relation(fields: [userId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
role OrganizationRole
@@unique([userId, organizationId])
@@map("users_organisations")
}
model Project extends Base {
slug String @unique
name String
isArchived Boolean @map("is_archived") @default(false)
organizationId String @map("organization_id")
organization Organization @relation(fields: [organizationId], references: [id])
databases Database[]
@@map("projects")
}
model Agent extends Base {
slug String @unique
name String
description String?
lastContact DateTime? @map("last_contact")
databases Database[]
// Restrict access to users with role "admin"
@@allow('all', auth().role == "admin")
@@map("agents")
}
enum Dbms {
postgresql
mysql
mongodb
}
model Database extends Base {
name String
dbms Dbms
generatedId String @unique
description String?
backupPolicy String? @map("backup_policy")
isWaitingForBackup Boolean? @default(false)
backupToRestore String?
agentId String @map("agent_id")
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
lastContact DateTime? @map("last_contact")
backups Backup[]
restorations Restoration[]
projectId String? @map("project_id")
project Project? @relation(fields: [projectId], references: [id])
@@map("databases")
}
enum Status {
waiting
ongoing
failed
success
}
model Backup extends Base {
status Status @default(waiting)
file String?
databaseId String @map("database_id")
database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
restorations Restoration[]
@@map("backups")
}
model Restoration extends Base {
status Status @default(waiting)
backupId String @map("backup_id")
backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade)
databaseId String? @map("database_id")
database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade)
@@map("restorations")
}
enum TypeStorage {
local
s3
}
model Settings extends Base {
storage TypeStorage @default(local)
name String @unique
s3EndPointUrl String?
s3AccessKeyId String?
s3SecretAccessKey String?
S3BucketName String?
smtpPassword String?
smtpFrom String?
smtpHost String?
smtpPort String?
smtpUser String?
// Restrict access to users with role "admin"
@@allow('all', auth().role == "admin")
@@map("settings")
}
-162
View File
@@ -1,162 +0,0 @@
import NextAuth, {CredentialsSignin} from "next-auth";
import {prisma} from "@/prisma";
import {PrismaAdapter} from "@auth/prisma-adapter";
import Credentials from "next-auth/providers/credentials";
import {env} from "@/env.mjs";
import GoogleProvider from "next-auth/providers/google";
export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
adapter: PrismaAdapter(prisma),
theme: {
logo: "/logo.png"
},
secret: env.NEXT_PUBLIC_SECRET,
debug: process.env.NODE_ENV !== "production",
providers: [
Credentials({
name: "Credentials",
credentials: {
email: {label: "Email", type: "email"},
password: {label: "Password", type: "password"},
},
authorize: async (credentials) => {
let user = null
const argon2 = require('argon2');
console.log(credentials);
user = await prisma.user.findFirst({
where: {
email: credentials.email,
}
})
if (!user || user.role === "pending") {
return null
}
const isValid = await argon2.verify(user.password, credentials.password)
if (!isValid) {
return null
}
return user;
},
}),
GoogleProvider({
clientId: env.AUTH_GOOGLE_ID,
clientSecret: env.AUTH_GOOGLE_SECRET,
allowDangerousEmailAccountLinking: true,
async profile(profile) {
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
}
})
console.log(users.length)
const role = users.length > 0 ? "pending" : "admin"
return {
role: role,
name: profile.name,
email: profile.email,
image: profile.picture,
email_verified: profile.email_verified,
authMethod: "google",
}
},
})
],
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
error: "/error"
},
callbacks: {
// session({session, user, token}) {
// // session.user.role = user.role
// // session.user.authMethod = user.authMethod;
// return session
// },
async jwt({token, trigger, session, user}) {
if (trigger === "update" && session) {
return {...token, ...session?.user};
}
return {...token, ...user};
},
async session({session, token, user}) {
session.user = token;
return session;
},
async signIn({account, user, profile}) {
const existingUser = await prisma.user.findFirst({
where: {email: user.email},
});
if (!existingUser) {
// Create a new user with a pending role
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
}
})
const role = users.length > 0 ? "pending" : "admin"
const newUser = await prisma.user.create({
data: {
email: user.email,
name: user.name,
image: user.image,
role: role,
authMethod: account.provider,
},
});
const defaultOrganization = await prisma.organization.findUnique({
where: {slug: "default"},
include: {users: {
include:{
user: true
},
where:{
user: {
deleted: {not: true},
}
}
}}
});
const organizationRole = defaultOrganization.users.length > 0 ? "member" : "admin"
await prisma.userOrganization.create({
data: {
userId: newUser.id,
organizationId: defaultOrganization.id,
role: organizationRole
},
});
return role !== "pending";
}
if (existingUser.role === "pending") {
return false; // Prevent login if role is pending
}
// Update auth method if user exists
await prisma.user.update({
where: {id: existingUser.id},
data: {
authMethod: account.provider,
},
});
return true; // Allow login
},
}
});
-29
View File
@@ -1,29 +0,0 @@
"use server"
import {baseAuth} from "@/auth/auth";
import {User} from "@prisma/client";
import {prisma} from "@/prisma";
export const currentUser = async () => {
const session = await baseAuth();
if (!session?.user) {
return null;
}
return session.user as User;
}
export const requiredCurrentUser = async () => {
const user = await currentUser();
if (!user) {
return null
}
const userDb = await prisma.user.findFirst({
where: {id: user.id},
})
if(!userDb){
return null
}
return user;
}
+6 -13
View File
@@ -1,16 +1,9 @@
import {twx} from "@/lib/twx";
import {cn} from "@/lib/utils";
import { twx } from "@/lib/twx";
import { cn } from "@/lib/utils";
export const LayoutAdmin = twx.div((props) => [`w-full h-screen flex flex-col gap-4 mx-auto `]);
export const Layout = twx.div((props) => [`max-w-5xl w-full flex-col flex gap-4 mx-auto px-4 `]);
export const LayoutAdmin = twx.div((props) => [
`w-full h-screen flex flex-col gap-4 mx-auto `
]);
export const Layout = twx.div((props)=>[
`max-w-5xl w-full flex-col flex gap-4 mx-auto px-4 `,
])
export const LayoutTitle = twx.h1((props) => [cn(`text-4xl font-bold mt-5 `, props.className)]);
export const LayoutTitle = twx.h1((props)=>[
cn(`text-4xl font-bold mt-5 `, props.className)
])
export const LayoutDescription = twx.p((props)=>[`text-lg text-muted-foreground`])
export const LayoutDescription = twx.p((props) => [`text-lg text-muted-foreground`]);
+53 -44
View File
@@ -2,56 +2,65 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+117 -101
View File
@@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
const AlertDialogPortal = AlertDialogPrimitive.Portal
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
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}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
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",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
)
}
const AlertDialogHeader = ({
function AlertDialogContent({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
const AlertDialogFooter = ({
function AlertDialogHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
+44 -37
View File
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-background text-foreground",
default: "bg-card text-card-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
@@ -19,41 +19,48 @@ const alertVariants = cva(
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }
+5 -1
View File
@@ -2,6 +2,10 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
}
export { AspectRatio }
+42 -39
View File
@@ -5,46 +5,49 @@ 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
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
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
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
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
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }
+20 -10
View File
@@ -1,20 +1,22 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
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",
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
@@ -23,13 +25,21 @@ const badgeVariants = cva(
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
+76 -83
View File
@@ -1,108 +1,101 @@
import * as React from "react"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
}
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
const BreadcrumbSeparator = ({
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
)
}
const BreadcrumbEllipsis = ({
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
+30 -28
View File
@@ -5,26 +5,27 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
@@ -34,24 +35,25 @@ const buttonVariants = cva(
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+51 -66
View File
@@ -1,72 +1,57 @@
"use client"
"use client";
import * as React from "react"
import { DayPicker } from "react-day-picker"
import * as React from "react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
export type CalendarProps = React.ComponentProps<typeof DayPicker>
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
)
function Calendar({ className, classNames, showOutsideDays = true, ...props }: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(buttonVariants({ variant: "outline" }), "size-7 bg-transparent p-0 opacity-50 hover:opacity-100"),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(buttonVariants({ variant: "ghost" }), "size-8 p-0 font-normal aria-selected:opacity-100"),
day_range_start: "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end: "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, children: _children, ...props }) => <ChevronLeftIcon className={cn("size-4", className)} {...props} />,
IconRight: ({ className, children: _children, ...props }) => <ChevronRightIcon className={cn("size-4", className)} {...props} />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar"
export { Calendar }
export { Calendar };
+82 -66
View File
@@ -2,75 +2,91 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+113 -135
View File
@@ -1,13 +1,12 @@
"use client"
import * as React from "react"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
@@ -42,124 +41,106 @@ function useCarousel() {
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
{children}
</div>
</CarouselContext.Provider>
)
}
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
@@ -169,20 +150,16 @@ const CarouselContent = React.forwardRef<
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
}
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
@@ -191,24 +168,25 @@ const CarouselItem = React.forwardRef<
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
}
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
"absolute size-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
@@ -216,28 +194,29 @@ const CarouselPrevious = React.forwardRef<
onClick={scrollPrev}
{...props}
>
<ArrowLeftIcon className="h-4 w-4" />
<ArrowLeftIcon />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
}
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
"absolute size-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
@@ -245,12 +224,11 @@ const CarouselNext = React.forwardRef<
onClick={scrollNext}
{...props}
>
<ArrowRightIcon className="h-4 w-4" />
<ArrowRightIcon />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
}
export {
type CarouselApi,
+190 -202
View File
@@ -34,25 +34,28 @@ function useChart() {
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
@@ -64,12 +67,11 @@ const ChartContainer = React.forwardRef<
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
@@ -102,219 +104,205 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
)
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
if (!value) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.value}
key={item.dataKey}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
)
ChartLegendContent.displayName = "ChartLegend"
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
+22 -20
View File
@@ -2,29 +2,31 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
+25 -3
View File
@@ -2,10 +2,32 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+2 -2
View File
@@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
<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",
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
<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",
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden 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}

Some files were not shown because too many files have changed in this diff Show More