mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
migration
This commit is contained in:
@@ -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
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"WillLuke.nextjs.hasPrompted": true
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
migrate:
|
||||
docker compose run --rm app sh -c "npx prisma migrate dev"
|
||||
docker compose run --rm app
|
||||
@@ -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
|
||||
[](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/
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+19
-24
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+54
-70
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth.handler);
|
||||
@@ -1,4 +0,0 @@
|
||||
import {handlers} from "@/auth/auth";
|
||||
|
||||
|
||||
export const {GET, POST} = handlers;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -10,7 +10,7 @@ echo " Community Edition v1.0.0 "
|
||||
echo " "
|
||||
|
||||
|
||||
npx prisma migrate deploy
|
||||
npx drizzle-kit migrate
|
||||
node server.js
|
||||
|
||||
exec "$@"
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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"
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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`]);
|
||||
|
||||
@@ -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
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user