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:
|
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 />)
|
<h3 align="center">Portabase</h3>
|
||||||
|
|
||||||
[//]: # (<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>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Backup / Restore your databases instances
|
Easily backup and restore your database instances with Portabase
|
||||||
<br />
|
<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 />
|
||||||
<br />
|
<br />
|
||||||
<a href="https://portabase.io">View Demo</a>
|
<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>
|
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
</p>
|
</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
|
## About The Project
|
||||||
|
|
||||||
Portabase is the server dashboard service working with portabase agents.
|
**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.
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
|
|
||||||
|
|
||||||
<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]
|
## 🚀 Getting Started
|
||||||
* [![Prisma][Prisma]][Prisma-url]
|
|
||||||
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- GETTING STARTED -->
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Install docker on your machine
|
Ensure Docker is installed on your machine before getting started.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Installation steps in order to use it locally and work on some features improvements
|
To run Portabase locally:
|
||||||
|
|
||||||
1. Clone the repo
|
1. Clone the repository:
|
||||||
```sh
|
```bash
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
git clone https://github.com/Soluce-Technologies/portabase
|
||||||
```
|
cd portabase
|
||||||
3. Start the project
|
```
|
||||||
```sh
|
2. Start the development environment:
|
||||||
docker compose up
|
```bash
|
||||||
```
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
---
|
||||||
|
|
||||||
|
## 🛠️ Usage
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
Portabase provides a dashboard to manage database instances and backups. It supports:
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [ ] Add Changelog
|
- PostgreSQL (current)
|
||||||
- [ ] Improve documentation and repositories
|
- MongoDB (coming soon)
|
||||||
- [ ] Add tests procedure
|
- MySQL (coming soon)
|
||||||
- [ ] Add Release File
|
|
||||||
- [ ] Security improvements
|
You can access the dashboard at `http://localhost:3000` after starting the project.
|
||||||
- [ ] Improve UX/UI
|
|
||||||
- [ ] Improve Workspace system
|
---
|
||||||
- [ ] Migrate to Drizzle ORM
|
|
||||||
- [ ] Multi locale support
|
## 🗺️ Roadmap
|
||||||
- [ ] Multi db support
|
|
||||||
|
- [ ] 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
|
- [x] PostgreSQL
|
||||||
- [ ] MongoDB
|
- [ ] MongoDB
|
||||||
- [ ] MySQL
|
- [ ] 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 -->
|
1. Fork the repository
|
||||||
## Contributing
|
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".
|
### Top Contributors
|
||||||
Don't forget to give the project a star! Thanks again!
|
|
||||||
|
|
||||||
1. Fork the Project
|
[](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
|
||||||
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
|
|
||||||
|
|
||||||
### Top contributors:
|
---
|
||||||
|
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors">
|
## Developer Notes
|
||||||
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<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 -->
|
### Semantic Versioning
|
||||||
## License
|
|
||||||
|
|
||||||
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 -->
|
### Zenstack Support
|
||||||
## Contact
|
|
||||||
|
|
||||||
Killian Larcher - killian.larcher@soluce-technologies.com
|
To regenerate schema:
|
||||||
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
|
||||||
|
|
||||||
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 -->
|
## 📄 License
|
||||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
||||||
|
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
|
[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
|
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
|
||||||
[Prisma-url]: https://www.prisma.io/
|
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
|
||||||
[NextJS-url]: https://nextjs.org/
|
[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 { 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 { 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<{}>) {
|
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 settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, { eq }) => eq(fields.name, "system"),
|
||||||
const users = await prisma.user.findMany({
|
});
|
||||||
where: {
|
|
||||||
// id: {
|
|
||||||
// not: user.id
|
|
||||||
// },
|
|
||||||
deleted: {not: true},
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const settings = await prisma.settings.findUnique({
|
|
||||||
where: {
|
|
||||||
name: "system"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Administration Panel</PageTitle>
|
||||||
Administration Panel
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="mt-10">
|
<PageContent className="mt-10">
|
||||||
<AdminTabs settings={settings} currentUser={user} users={users}/>
|
<AdminTabs settings={settings!} currentUser={user} users={users} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
|
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
|
||||||
import {prisma} from "@/prisma";
|
import { notFound } from "next/navigation";
|
||||||
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<{
|
const agent = await db.query.agent.findFirst({
|
||||||
agentId: string;
|
where: (fields, { eq }) => eq(fields.id, agentId),
|
||||||
}>) {
|
|
||||||
|
|
||||||
const {agentId} = await props.params
|
|
||||||
const agent = await prisma.agent.findUnique({
|
|
||||||
where: {
|
|
||||||
id: agentId,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
@@ -23,13 +22,11 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Edit {agent.name}</PageTitle>
|
||||||
Edit {agent.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<AgentForm defaultValues={agent} agentId={agent.id}/>
|
<AgentForm defaultValues={agent} agentId={agent.id} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
|
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
|
||||||
import {currentUser} from "@/auth/current-user";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const user = await currentUser();
|
|
||||||
|
|
||||||
if(user.role != "admin"){
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Create new agent</PageTitle>
|
||||||
Create new agent
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<AgentForm/>
|
<AgentForm />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {AgentCard} from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
|
import { AgentCard } from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {Button} from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {prisma} from "@/prisma";
|
import { notFound } from "next/navigation";
|
||||||
import {notFound} from "next/navigation";
|
import { db } from "@/db";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Agents</PageTitle>
|
||||||
Agents
|
|
||||||
</PageTitle>
|
|
||||||
{agents.length > 0 && (
|
{agents.length > 0 && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={"/dashboard/agents/new"}>
|
<Link href={"/dashboard/agents/new"}>
|
||||||
@@ -33,21 +28,17 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="mt-10">
|
<PageContent className="mt-10">
|
||||||
{agents.length > 0 ?
|
{agents.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
|
||||||
data={agents}
|
) : (
|
||||||
cardItem={AgentCard}
|
|
||||||
cardsPerPage={4}
|
|
||||||
numberOfColumns={1}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<Link
|
<Link
|
||||||
href={"/dashboard/agents/new"}
|
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
|
Create new Agent
|
||||||
</Link>
|
</Link>
|
||||||
}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import React from "react";
|
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 !== "superadmin" && user!.role !== "admin") {
|
||||||
|
notFound();
|
||||||
if(user.role != "admin"){
|
|
||||||
notFound()
|
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<>
|
return <>{children}</>;
|
||||||
{children}
|
}
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,55 +1,41 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
import { notFound } from "next/navigation";
|
||||||
import {prisma} from "@/prisma";
|
import { RestoreForm } from "@/components/wrappers/dashboard/database/RestoreForm";
|
||||||
import {notFound} from "next/navigation";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
|
|
||||||
|
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { database, backup } from "@/db/schema";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(
|
||||||
databaseId: string;
|
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()
|
if (!dbToRestore) {
|
||||||
|
|
||||||
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) {
|
|
||||||
notFound();
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Restore {dbToRestore.name}</PageTitle>
|
||||||
Restore {database.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/>
|
<RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import {PageParams} from "@/types/next";
|
"use client";
|
||||||
import {redirect} from "next/navigation";
|
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 (
|
||||||
// return (
|
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||||
// <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="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 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>
|
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
||||||
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
|
{JSON.stringify(activeOrganization)}
|
||||||
// </div>
|
{activeOrganization?.name}
|
||||||
// )
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
+19
-24
@@ -1,39 +1,34 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
import { notFound } from "next/navigation";
|
||||||
import {prisma} from "@/prisma";
|
import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
|
||||||
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<{
|
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
||||||
databaseId: string;
|
const { databaseId } = await props.params;
|
||||||
}>) {
|
|
||||||
|
|
||||||
const {databaseId} = await props.params
|
const dbItem = await db.query.database.findFirst({
|
||||||
|
where: eq(database.id, databaseId),
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
const database = await prisma.database.findUnique({
|
|
||||||
where: {
|
|
||||||
id: databaseId,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!database) {
|
if (!dbItem) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Edit {dbItem.name}</PageTitle>
|
||||||
Edit {database.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<DatabaseForm databaseId={databaseId} defaultValues={database}/>
|
<DatabaseForm
|
||||||
|
databaseId={databaseId}
|
||||||
|
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-70
@@ -1,91 +1,75 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {prisma} from "@/prisma";
|
import { notFound } from "next/navigation";
|
||||||
import {notFound} from "next/navigation";
|
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import { BackupButton } from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
import { DatabaseTabs } from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
|
||||||
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
|
import { DatabaseKpi } from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
|
||||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
|
import { EditButton } from "@/components/wrappers/dashboard/database/EditButton/EditButton";
|
||||||
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
|
import { CronButton } from "@/components/wrappers/dashboard/database/CronButton/CronButton";
|
||||||
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 }>) {
|
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
||||||
|
const { databaseId } = await props.params;
|
||||||
|
|
||||||
const {databaseId} = await props.params
|
const dbItem = await db.query.database.findFirst({
|
||||||
const database = await prisma.database.findUnique({
|
where: eq(drizzleDatabase.id, databaseId),
|
||||||
where: {
|
});
|
||||||
id: databaseId,
|
|
||||||
},
|
if (!dbItem) {
|
||||||
})
|
notFound();
|
||||||
if (!database) {
|
|
||||||
notFound()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backups = await db.query.backup.findMany({
|
||||||
const backups = await prisma.backup.findMany({
|
where: eq(drizzleBackup.databaseId, dbItem.id),
|
||||||
where: {
|
with: {
|
||||||
databaseId: database.id
|
restorations: true,
|
||||||
},
|
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
|
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
const successfulBackups = await prisma.backup.count({
|
|
||||||
where: {
|
const restorations = await db.query.restoration.findMany({
|
||||||
databaseId: databaseId,
|
where: eq(drizzleRestoration.databaseId, dbItem.id),
|
||||||
status: 'success',
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{database.name}
|
{dbItem.name}
|
||||||
<EditButton/>
|
<EditButton />
|
||||||
<CronButton database={database}/>
|
<CronButton database={dbItem} />
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
<PageActions className="justify-between">
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
<BackupButton disable={isAlreadyBackup} databaseId={databaseId} />
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</div>
|
</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">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
<DatabaseKpi
|
<DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups} />
|
||||||
successRate={successRate}
|
<DatabaseTabs database={dbItem} isAlreadyRestore={isAlreadyRestore} backups={backups} restorations={restorations} />
|
||||||
database={database}
|
|
||||||
totalBackups={totalBackups}
|
|
||||||
/>
|
|
||||||
<DatabaseTabs
|
|
||||||
database={database}
|
|
||||||
isAlreadyRestore={isAlreadyRestore}
|
|
||||||
backups={backups}
|
|
||||||
restorations={restorations}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</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 { db } from "@/db";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { eq } from "drizzle-orm";
|
||||||
import {prisma} from "@/prisma";
|
import { project as drizzleProject, organization as drizzleOrganization, DatabaseWith } from "@/db/schema";
|
||||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
||||||
|
const { projectId } = await props.params;
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
const proj = await db.query.project.findFirst({
|
||||||
projectId: string;
|
where: eq(drizzleProject.id, projectId),
|
||||||
}>) {
|
with: {
|
||||||
|
databases: true,
|
||||||
const {projectId} = await props.params
|
|
||||||
|
|
||||||
const project = await prisma.project.findUnique({
|
|
||||||
where: {
|
|
||||||
id: projectId,
|
|
||||||
},
|
},
|
||||||
include: {
|
|
||||||
databases: {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!proj) {
|
||||||
notFound();
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Edit {proj.name}</PageTitle>
|
||||||
Edit {project.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<ProjectForm organization={organization} databases={availableDatabases} defaultValues={project}
|
<ProjectForm
|
||||||
projectId={project.id}/>
|
organization={org}
|
||||||
|
databases={availableDatabases}
|
||||||
|
defaultValues={{ name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id) }}
|
||||||
|
projectId={proj.id}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,70 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import {prisma} from "@/prisma";
|
import { GearIcon } from "@radix-ui/react-icons";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
|
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
||||||
import {notFound} from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
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();
|
||||||
|
|
||||||
|
if (currentOrganizationSlug !== organizationSlug) {
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
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({
|
const proj = await db.query.project.findFirst({
|
||||||
where: {
|
where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
||||||
id: projectId,
|
with: {
|
||||||
isArchived: {not: true}
|
databases: true,
|
||||||
},
|
},
|
||||||
include:{
|
});
|
||||||
databases: {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if(!project){
|
if (!proj) notFound();
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{project.name}
|
{proj.name}
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
<Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/${currentOrganizationSlug}/projects/${proj.id}/edit`}>
|
||||||
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
|
<GearIcon className="w-7 h-7" />
|
||||||
<GearIcon className="w-7 h-7"/>
|
|
||||||
</Link>
|
</Link>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
<PageActions className="justify-between">
|
||||||
<ButtonDeleteProject
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"} />
|
||||||
projectId={projectId}
|
|
||||||
text={"Delete Project"}
|
|
||||||
/>
|
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</div>
|
</div>
|
||||||
<PageDescription>
|
|
||||||
The list of associated databases
|
<PageDescription>The list of associated databases</PageDescription>
|
||||||
</PageDescription>
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{project.databases.length > 0 ?
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={project.databases}
|
data={proj.databases}
|
||||||
organizationSlug={organizationSlug}
|
organizationSlug={organizationSlug}
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={4}
|
||||||
numberOfColumns={1}
|
numberOfColumns={1}
|
||||||
extendedProps={project}
|
extendedProps={proj}
|
||||||
/>
|
/>
|
||||||
:
|
) : null}
|
||||||
null
|
|
||||||
}
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {prisma} from "@/prisma";
|
import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
||||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
import { notFound } from "next/navigation";
|
||||||
import {notFound} from "next/navigation";
|
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
|
||||||
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}>) {
|
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||||
const {slug: organizationSlug} = await props.params
|
const { slug: organizationSlug } = await props.params;
|
||||||
|
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
||||||
if(currentOrganizationSlug != organizationSlug) {
|
if (currentOrganizationSlug !== organizationSlug) {
|
||||||
notFound()
|
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({
|
const org = await db.query.organization.findFirst({
|
||||||
where: {
|
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
|
||||||
projectId: null
|
});
|
||||||
},
|
|
||||||
include: {
|
|
||||||
agent: {}
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const organization = await prisma.organization.findFirst({
|
if (!org) notFound();
|
||||||
where: {
|
|
||||||
slug: currentOrganizationSlug,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Create new project</PageTitle>
|
||||||
Create new project
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<ProjectForm databases={availableDatabases} organization={organization}/>
|
<ProjectForm databases={availableDatabases} organization={org} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,34 @@
|
|||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
|
|
||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {prisma} from "@/prisma";
|
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import { Button } from "@/components/ui/button";
|
||||||
import {Button} from "@/components/ui/button";
|
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { ProjectCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
|
||||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
|
import { db } from "@/db";
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
import { notFound } from "next/navigation";
|
||||||
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 organization = await getOrganization(organizationSlug);
|
||||||
const {slug: organizationSlug} = await props.params
|
|
||||||
|
|
||||||
|
if (!organization || organization?.slug !== organizationSlug) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const projects = await prisma.project.findMany({
|
const projects = await db.query.project.findMany({
|
||||||
where: {
|
where: (project, { eq, and, not }) => and(eq(project.organizationId, organization.id), not(eq(project.isArchived, true))),
|
||||||
isArchived: {not: true},
|
with: {
|
||||||
|
databases: true,
|
||||||
organization: {
|
|
||||||
slug: organizationSlug,
|
|
||||||
// users: {
|
|
||||||
// some: {
|
|
||||||
// userId: user.id
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
include: {
|
});
|
||||||
databases: {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Projects</PageTitle>
|
||||||
Projects
|
|
||||||
</PageTitle>
|
|
||||||
{projects.length > 0 && (
|
{projects.length > 0 && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={`/dashboard/${organizationSlug}/projects/new`}>
|
<Link href={`/dashboard/${organizationSlug}/projects/new`}>
|
||||||
@@ -45,28 +36,20 @@ export default async function RoutePage(props: PageParams<{slug: string}>) {
|
|||||||
</Link>
|
</Link>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent className="mt-10">
|
<PageContent className="mt-10">
|
||||||
|
{projects.length > 0 ? (
|
||||||
{projects.length > 0 ?
|
<CardsWithPagination organizationSlug={organizationSlug} data={projects} cardItem={ProjectCard} cardsPerPage={4} numberOfColumns={1} />
|
||||||
<CardsWithPagination
|
) : (
|
||||||
organizationSlug={organizationSlug}
|
|
||||||
data={projects}
|
|
||||||
cardItem={ProjectCard}
|
|
||||||
cardsPerPage={4}
|
|
||||||
numberOfColumns={1}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/${organizationSlug}/projects/new`}
|
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
|
Create new Project
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
}
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,57 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {prisma} from "@/prisma";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
||||||
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-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({
|
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
||||||
where: {
|
if (currentOrganizationSlug !== organizationSlug) {
|
||||||
organization: {
|
notFound();
|
||||||
slug: "default"
|
}
|
||||||
}
|
|
||||||
},
|
const org = await db.query.organization.findFirst({
|
||||||
|
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
|
||||||
});
|
});
|
||||||
|
|
||||||
const backupsEvolution = await prisma.backup.findMany({
|
if (!org) notFound();
|
||||||
select: {
|
|
||||||
|
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,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: [asc(drizzleBackup.id)],
|
||||||
createdAt: "asc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupsRate = await prisma.backup.groupBy({
|
|
||||||
by: ['createdAt', 'status'],
|
|
||||||
where: {
|
|
||||||
status: {
|
|
||||||
in: ['success', 'failed'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_count: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle>Statistics</PageTitle>
|
||||||
Statistics
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
@@ -50,23 +59,19 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Projects</CardTitle>
|
<CardTitle>Projects</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>{projectsCount}</CardContent>
|
||||||
{projectsCount}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>KPI 2</CardTitle>
|
<CardTitle>KPI 2</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent></CardContent>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>KPI 3</CardTitle>
|
<CardTitle>KPI 3</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent></CardContent>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<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>
|
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<EvolutionLineChart data={backupsEvolution}/>
|
<EvolutionLineChart data={backupsEvolution} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@@ -83,11 +88,11 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<CardTitle>Success rate of backups</CardTitle>
|
<CardTitle>Success rate of backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<PercentageLineChart data={backupsRate}/>
|
<PercentageLineChart data={backupsRate} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,31 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {redirect} from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar"
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar";
|
import { AppSidebar } from "@/components/wrappers/dashboard/sideBar/app-sidebar";
|
||||||
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
|
import { Header } from "@/features/layout/Header";
|
||||||
import {Header} from "@/features/layout/Header";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
|
|
||||||
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const user = await currentUser();
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
|
|
||||||
if (!user) redirect('/login')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <HydrationZustand>
|
// <HydrationZustand>
|
||||||
// <GlobalStoreProvider>
|
// <GlobalStoreProvider>
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<AppSidebar/>
|
<AppSidebar />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header/>
|
<Header />
|
||||||
<main className="h-full">
|
<main className="h-full">{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</>
|
</>
|
||||||
// </GlobalStoreProvider>
|
// </GlobalStoreProvider>
|
||||||
// </HydrationZustand>
|
// </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() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||||
<LoadingSpinner size={50} />
|
<LoadingSpinner size={50} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,62 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import {notFound} from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
import { UserForm } from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
|
||||||
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {prisma} from "@/prisma";
|
import { ButtonDeleteAccount } from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import { AvatarWithUpload } from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
|
||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
|
//import { getAccounts, getSessions } from "@/lib/auth/auth";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
const user = await currentUser();
|
||||||
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
notFound()
|
return notFound();
|
||||||
}
|
}
|
||||||
const userInfo = await prisma.user.findUnique({
|
|
||||||
where: {
|
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
|
||||||
email: user.email
|
return notFound();
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
// const sessions = await getSessions();
|
||||||
|
// const accounts = await getAccounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
<AvatarWithUpload user={userInfo}/>
|
<AvatarWithUpload
|
||||||
{user.name}
|
user={{
|
||||||
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge>
|
...user,
|
||||||
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge>
|
image: user.image ?? null,
|
||||||
</PageTitle>
|
role: user.role ?? null,
|
||||||
<PageActions className="mt-2 hidden sm:block">
|
banned: user.banned ?? null,
|
||||||
<ButtonDeleteAccount text="Delete my account"/>
|
banReason: user.banReason ?? null,
|
||||||
</PageActions>
|
banExpires: user.banExpires ?? null,
|
||||||
</div>
|
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>
|
<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">
|
<div className="mt-4 sm:hidden">
|
||||||
<ButtonDeleteAccount text="Delete my account" />
|
<ButtonDeleteAccount text="Delete my account" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
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)]">
|
<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>
|
<h1 className="font-bold text-4xl leading-tight">Home landing page</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import {redirect} from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import {currentUser} from "@/auth/current-user";
|
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
|
|
||||||
|
|
||||||
export default async function Index() {
|
export default async function Index() {
|
||||||
const user = await currentUser()
|
const user = await currentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
||||||
redirect(`/dashboard/${currentOrganizationSlug}/projects`)
|
redirect(`/dashboard/${currentOrganizationSlug}/projects`);
|
||||||
}
|
}
|
||||||
redirect("/login")
|
redirect("/login");
|
||||||
//Do not delete
|
//Do not delete
|
||||||
// return (
|
// return (
|
||||||
// <Home/>
|
// <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 path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: Request,
|
|
||||||
{params}: { params: Promise<{ fileName: string }> }
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
|
||||||
try {
|
try {
|
||||||
const fileName = (await params).fileName
|
const fileName = (await params).fileName;
|
||||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||||
|
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
try {
|
try {
|
||||||
await fs.access(filePath); // Ensures the file exists
|
await fs.access(filePath); // Ensures the file exists
|
||||||
} catch {
|
} catch {
|
||||||
return NextResponse.json({error: "File not found"}, {status: 404});
|
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the file
|
// Read the file
|
||||||
@@ -29,6 +25,6 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading file:", 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 = {
|
export type BodyInit = {
|
||||||
initialize: boolean
|
initialize: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: Request,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const body: BodyInit = await request.json();
|
const body: BodyInit = await request.json();
|
||||||
|
|
||||||
console.log(body)
|
console.log(body);
|
||||||
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
@@ -24,9 +18,6 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in POST initialization:", error);
|
console.error("Error in POST initialization:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
{ 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">
|
// <div className="flex h-screen w-full flex-col items-center justify-center">
|
||||||
// <a
|
// <a
|
||||||
// href="#"
|
// 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">
|
// <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
|
// Something went wrong
|
||||||
@@ -63,7 +63,7 @@ function AuthErrorPageContent() {
|
|||||||
<div className="flex h-screen w-full flex-col items-center justify-center">
|
<div className="flex h-screen w-full flex-col items-center justify-center">
|
||||||
<a
|
<a
|
||||||
href="#"
|
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">
|
<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
|
Something went wrong
|
||||||
|
|||||||
+127
-90
@@ -1,105 +1,142 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@import "tw-animate-css";
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
/*body {*/
|
@custom-variant dark (&:is(.dark *));
|
||||||
/* font-family: Arial, Helvetica, sans-serif;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
@layer utilities {
|
@theme inline {
|
||||||
/*.text-balance {*/
|
--color-background: var(--background);
|
||||||
/* text-wrap: balance;*/
|
--color-foreground: var(--foreground);
|
||||||
/*}*/
|
--font-sans: var(--font-geist-sans);
|
||||||
.popover-content-width-full {
|
--font-mono: var(--font-geist-mono);
|
||||||
width: var(--radix-popover-trigger-width);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
max-height: var(--radix-popover-content-available-height);
|
--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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
@layer base {
|
--background: oklch(1 0 0);
|
||||||
:root {
|
--foreground: oklch(0.145 0 0);
|
||||||
--background: 0 0% 100%;
|
--card: oklch(1 0 0);
|
||||||
--foreground: 240 10% 3.9%;
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--card: 0 0% 100%;
|
--popover: oklch(1 0 0);
|
||||||
--card-foreground: 240 10% 3.9%;
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--popover: 0 0% 100%;
|
--primary: oklch(0.205 0 0);
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--primary: 240 5.9% 10%;
|
--secondary: oklch(0.97 0 0);
|
||||||
--primary-foreground: 0 0% 98%;
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--secondary: 240 4.8% 95.9%;
|
--muted: oklch(0.97 0 0);
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
--muted: 240 4.8% 95.9%;
|
--accent: oklch(0.97 0 0);
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--accent: 240 4.8% 95.9%;
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--border: oklch(0.922 0 0);
|
||||||
--destructive: 0 84.2% 60.2%;
|
--input: oklch(0.922 0 0);
|
||||||
--destructive-foreground: 0 0% 98%;
|
--ring: oklch(0.708 0 0);
|
||||||
--border: 240 5.9% 90%;
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--input: 240 5.9% 90%;
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--ring: 240 5.9% 10%;
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
--radius: 0.5rem;
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--chart-1: 12 76% 61%;
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--chart-2: 173 58% 39%;
|
--sidebar: oklch(0.985 0 0);
|
||||||
--chart-3: 197 37% 24%;
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
--chart-4: 43 74% 66%;
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
--chart-5: 27 87% 67%;
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
--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%;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-15
@@ -1,13 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type {Metadata} from "next";
|
import type { Metadata } from "next";
|
||||||
import {Inter} from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import {Providers} from "./providers";
|
import { Providers } from "./providers";
|
||||||
import {cn} from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
const inter = Inter({subsets: ["latin"]});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Portabase",
|
title: "Portabase",
|
||||||
@@ -15,19 +14,15 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body className={cn(inter.className, "h-full")}>
|
||||||
className={cn(inter.className, "h-full")}
|
<Providers>{children}</Providers>
|
||||||
>
|
</body>
|
||||||
<Providers>
|
|
||||||
{children}
|
|
||||||
</Providers>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -1,6 +1,5 @@
|
|||||||
import {redirect, useRouter} from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
console.log("not found");
|
redirect("/dashboard/profile");
|
||||||
redirect("/")
|
}
|
||||||
}
|
|
||||||
|
|||||||
+14
-23
@@ -1,32 +1,23 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {PropsWithChildren, Suspense} from "react";
|
import { PropsWithChildren, Suspense } from "react";
|
||||||
import {ThemeProvider} from "@/features/theme/theme-provider";
|
import { ThemeProvider } from "@/features/theme/theme-provider";
|
||||||
|
|
||||||
import {Toaster} from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import {SessionProvider} from "next-auth/react";
|
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export const Providers = (props: ProviderProps) => {
|
export const Providers = (props: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<SessionProvider>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<ThemeProvider
|
<QueryClientProvider client={queryClient}>
|
||||||
attribute="class"
|
<Toaster />
|
||||||
defaultTheme="system"
|
{props.children}
|
||||||
enableSystem
|
</QueryClientProvider>
|
||||||
// disableTransitionOnChange
|
</ThemeProvider>
|
||||||
>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Toaster/>
|
|
||||||
{props.children}
|
|
||||||
</QueryClientProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</SessionProvider>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|||||||
+68
-59
@@ -1,68 +1,77 @@
|
|||||||
name: portabase-dev
|
name: portabase-dev
|
||||||
|
|
||||||
services:
|
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:
|
develop:
|
||||||
build:
|
watch:
|
||||||
context: .
|
- action: sync
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
path: .
|
||||||
target: dev
|
target: /app
|
||||||
ports:
|
- action: rebuild
|
||||||
- '8887:80'
|
path: package.json
|
||||||
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}
|
|
||||||
|
|
||||||
|
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:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
minio_data:
|
||||||
|
pgadmin-data:
|
||||||
|
|||||||
@@ -3,11 +3,8 @@
|
|||||||
npm install -g npm@11.0.0
|
npm install -g npm@11.0.0
|
||||||
npm -v
|
npm -v
|
||||||
|
|
||||||
|
|
||||||
current_date=$(date +"%Y-%m-%d")
|
|
||||||
npx zenstack generate
|
npx zenstack generate
|
||||||
npx prisma migrate dev --name $current_date
|
npx drizzle-kit migrate
|
||||||
|
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
@@ -10,7 +10,7 @@ echo " Community Edition v1.0.0 "
|
|||||||
echo " "
|
echo " "
|
||||||
|
|
||||||
|
|
||||||
npx prisma migrate deploy
|
npx drizzle-kit migrate
|
||||||
node server.js
|
node server.js
|
||||||
|
|
||||||
exec "$@"
|
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 {
|
import { Button as ReactEmailButton } from "@react-email/components";
|
||||||
Button as ReactEmailButton
|
import { ComponentPropsWithoutRef } from "react";
|
||||||
} from "@react-email/components";
|
|
||||||
import {ComponentPropsWithoutRef} from "react";
|
|
||||||
|
|
||||||
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
|
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
|
||||||
return (
|
return <ReactEmailButton className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline " {...props} />;
|
||||||
<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 {
|
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components";
|
||||||
Body,
|
|
||||||
Container,
|
|
||||||
Head,
|
|
||||||
Html,
|
|
||||||
Img,
|
|
||||||
Preview,
|
|
||||||
Section,
|
|
||||||
Tailwind,
|
|
||||||
} from "@react-email/components";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const baseUrl = getServerUrl();
|
const baseUrl = getServerUrl();
|
||||||
|
|
||||||
@@ -22,19 +11,10 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
|
|||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
|
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
|
||||||
<Body className="bg-gray-100 py-4"
|
<Body className="bg-gray-100 py-4" style={{ fontFamily: "Arial, sans-serif" }}>
|
||||||
style={{fontFamily: "Arial, sans-serif"}}>
|
|
||||||
|
|
||||||
<Container className="bg-white border border-gray-200 p-12">
|
<Container className="bg-white border border-gray-200 p-12">
|
||||||
<Img
|
<Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
|
||||||
src={`${baseUrl}/logo-title-black.png`}
|
<Section>{children}</Section>
|
||||||
width="200"
|
|
||||||
height="auto"
|
|
||||||
alt="Logo"
|
|
||||||
/>
|
|
||||||
<Section>
|
|
||||||
{children}
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
@@ -42,6 +22,4 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default EmailLayout;
|
export default EmailLayout;
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ import * as React from "react";
|
|||||||
export default function HelloEmail() {
|
export default function HelloEmail() {
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Button
|
<Button href="https://example.com" style={{ background: "#000", color: "#fff" }}>
|
||||||
href="https://example.com"
|
Hello,
|
||||||
style={{ background: "#000", color: "#fff" }}
|
|
||||||
>
|
|
||||||
Hello Charles
|
|
||||||
</Button>
|
</Button>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,16 @@
|
|||||||
import {
|
import { Text } from "@react-email/components";
|
||||||
Text,
|
|
||||||
} from "@react-email/components";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import EmailLayout from "./EmailLayout";
|
import EmailLayout from "./EmailLayout";
|
||||||
import {Button} from "@react-email/button";
|
import { Button } from "@react-email/button";
|
||||||
|
|
||||||
|
|
||||||
export const TestEmailSettings = () => {
|
export const TestEmailSettings = () => {
|
||||||
return (
|
return (
|
||||||
<EmailLayout
|
<EmailLayout preview="Email Setup">
|
||||||
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>
|
||||||
<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>
|
</EmailLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default TestEmailSettings;
|
export default TestEmailSettings;
|
||||||
|
|||||||
+4
-11
@@ -1,13 +1,6 @@
|
|||||||
import {
|
import { Text as ReactEmailText } from "@react-email/components";
|
||||||
Text as ReactEmailText
|
import { ComponentPropsWithoutRef } from "react";
|
||||||
} from "@react-email/components";
|
|
||||||
import {ComponentPropsWithoutRef} from "react";
|
|
||||||
|
|
||||||
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
|
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
|
||||||
return (
|
return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
|
||||||
<ReactEmailText
|
};
|
||||||
className="text-base font-light leading-8 text-green-800 "
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
import {init} from "@/utils/init";
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
const init = await import("@/utils/init");
|
||||||
export function register() {
|
init.init();
|
||||||
init();
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-22
@@ -1,43 +1,54 @@
|
|||||||
import {NextRequest, NextResponse} from 'next/server'
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
|
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
||||||
import {errorHandler} from "@/middleware/errorHandler";
|
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();
|
const url = request.nextUrl.clone();
|
||||||
|
|
||||||
// Exclude `/api/auth` and its subpaths
|
if (url.pathname.startsWith("/dashboard")) {
|
||||||
if (url.pathname.startsWith('/api/auth')) {
|
const session = await auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.redirect(new URL("/login", request.url));
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
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);
|
const routeExists = checkRouteExists(url.pathname);
|
||||||
// If the route does not exist, return a 404 JSON response
|
// If the route does not exist, return a 404 JSON response
|
||||||
if (!routeExists) {
|
if (!routeExists) {
|
||||||
return new NextResponse(
|
return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
|
||||||
JSON.stringify({ message: "This API route does not exist.", status: 404 }),
|
status: 404,
|
||||||
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
headers: { "Content-Type": "application/json" },
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try{
|
try {
|
||||||
loggingMiddleware(request);
|
loggingMiddleware(request);
|
||||||
}catch(err){
|
} catch (err) {
|
||||||
errorHandler(err)
|
errorHandler(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
// Function to check if the route exists (supports dynamic routes)
|
// Function to check if the route exists (supports dynamic routes)
|
||||||
function checkRouteExists(pathname) {
|
function checkRouteExists(pathname: string) {
|
||||||
// Define static and dynamic routes with patterns
|
// Define static and dynamic routes with patterns
|
||||||
const routePatterns = [
|
const routePatterns = [
|
||||||
//do not delete
|
//do not delete
|
||||||
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
|
// /^\/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\/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\/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\/[^/]+\/backup\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||||
/^\/api\/files\/[^/]+\/?$/,
|
/^\/api\/files\/[^/]+\/?$/,
|
||||||
@@ -45,13 +56,15 @@ function checkRouteExists(pathname) {
|
|||||||
/^\/api\/events\/?$/,
|
/^\/api\/events\/?$/,
|
||||||
/^\/api\/init\/?$/,
|
/^\/api\/init\/?$/,
|
||||||
];
|
];
|
||||||
return routePatterns.some(pattern => pattern.test(pathname));
|
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
runtime: "nodejs",
|
||||||
matcher: [
|
matcher: [
|
||||||
// '/api/agent/:path*',
|
// '/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: NextConfig = {
|
||||||
const nextConfig = {
|
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
@@ -9,12 +9,11 @@ const nextConfig = {
|
|||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: process.env.NODE_ENV === 'production',
|
removeConsole: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
nodeMiddleware: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+112
-104
@@ -1,106 +1,114 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "0.1.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 80",
|
"dev": "next dev -p 80",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"email": "email dev"
|
"email": "email dev",
|
||||||
},
|
"db:generate": "drizzle-kit generate",
|
||||||
"dependencies": {
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"@auth/prisma-adapter": "^2.7.4",
|
"db:drop": "drizzle-kit drop",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||||
"@prisma/client": "^6.3.0",
|
},
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
"@radix-ui/react-accordion": "^1.2.10",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.9",
|
||||||
"@radix-ui/react-context-menu": "^2.2.5",
|
"@radix-ui/react-checkbox": "^1.3.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.5",
|
"@radix-ui/react-collapsible": "^1.1.10",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
"@radix-ui/react-context-menu": "^2.2.14",
|
||||||
"@radix-ui/react-hover-card": "^1.1.5",
|
"@radix-ui/react-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-hover-card": "^1.1.13",
|
||||||
"@radix-ui/react-menubar": "^1.1.5",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.4",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-popover": "^1.1.5",
|
"@radix-ui/react-menubar": "^1.1.14",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||||
"@radix-ui/react-radio-group": "^1.2.2",
|
"@radix-ui/react-popover": "^1.1.13",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
"@radix-ui/react-progress": "^1.1.6",
|
||||||
"@radix-ui/react-select": "^2.1.5",
|
"@radix-ui/react-radio-group": "^1.3.6",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||||
"@radix-ui/react-slider": "^1.2.2",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.6",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-slider": "^1.3.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-toast": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.4",
|
||||||
"@radix-ui/react-toggle": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.11",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
"@radix-ui/react-toast": "^1.2.13",
|
||||||
"@radix-ui/react-tooltip": "^1.1.7",
|
"@radix-ui/react-toggle": "^1.1.8",
|
||||||
"@react-email/components": "^0.0.32",
|
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@radix-ui/react-tooltip": "^1.2.6",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@react-email/components": "^0.0.41",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@t3-oss/env-nextjs": "^0.13.4",
|
||||||
"@types/ws": "^8.5.13",
|
"@tanstack/react-query": "^5.76.1",
|
||||||
"@zenstackhq/runtime": "2.10.2",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"argon2": "^0.41.1",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"bcrypt": "^5.1.1",
|
"@types/ws": "^8.18.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"clsx": "^2.1.1",
|
"argon2": "^0.43.0",
|
||||||
"cmdk": "^1.0.4",
|
"bcrypt": "^6.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"better-auth": "^1.2.8",
|
||||||
"dockerode": "^4.0.2",
|
"class-variance-authority": "^0.7.1",
|
||||||
"embla-carousel-react": "^8.3.1",
|
"clsx": "^2.1.1",
|
||||||
"input-otp": "^1.4.2",
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.474.0",
|
"date-fns": "^4.1.0",
|
||||||
"minio": "^8.0.2",
|
"dockerode": "^4.0.6",
|
||||||
"next": "15.1.7",
|
"dotenv": "^16.5.0",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"drizzle-orm": "^0.43.1",
|
||||||
"next-safe-action": "^7.10.2",
|
"drizzle-zod": "^0.7.1",
|
||||||
"next-themes": "^0.4.4",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"nodemailer": "^6.10.0",
|
"input-otp": "^1.4.2",
|
||||||
"npm-check-updates": "^17.1.13",
|
"lucide-react": "^0.510.0",
|
||||||
"react": "19.0.0",
|
"minio": "^8.0.5",
|
||||||
"react-day-picker": "^9.5.1",
|
"next": "^15.4.0-canary.36",
|
||||||
"react-dom": "19.0.0",
|
"next-safe-action": "^7.10.8",
|
||||||
"react-email": "^3.0.6",
|
"next-themes": "^0.4.6",
|
||||||
"react-hook-form": "^7.54.2",
|
"nodemailer": "^7.0.3",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"npm-check-updates": "^18.0.1",
|
||||||
"react-twc": "^1.4.2",
|
"pg": "^8.16.0",
|
||||||
"recharts": "^2.15.1",
|
"react": "19.1.0",
|
||||||
"socket.io": "^4.8.1",
|
"react-day-picker": "9.7.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"react-dom": "19.1.0",
|
||||||
"sonner": "^1.7.3",
|
"react-dropzone": "^14.3.8",
|
||||||
"tailwind-merge": "^3.0.1",
|
"react-email": "^4.0.13",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"react-hook-form": "^7.56.3",
|
||||||
"uuid": "^11.0.5",
|
"react-resizable-panels": "^3.0.2",
|
||||||
"vaul": "^1.1.2",
|
"react-twc": "^1.4.2",
|
||||||
"ws": "^8.18.0",
|
"recharts": "^2.15.3",
|
||||||
"zod": "^3.24.1"
|
"socket.io": "^4.8.1",
|
||||||
},
|
"socket.io-client": "^4.8.1",
|
||||||
"devDependencies": {
|
"sonner": "^2.0.3",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3",
|
"tailwind-merge": "^3.3.0",
|
||||||
"@types/node": "^22",
|
"uuid": "^11.1.0",
|
||||||
"@types/pg": "^8",
|
"vaul": "^1.1.2",
|
||||||
"@types/react": "^19",
|
"ws": "^8.18.2",
|
||||||
"@types/react-dom": "^19",
|
"zod": "^3.24.4"
|
||||||
"@zenstackhq/openapi": "^2.10.2",
|
},
|
||||||
"@zenstackhq/tanstack-query": "^2.10.2",
|
"devDependencies": {
|
||||||
"eslint": "^8",
|
"@iconify/react": "^6.0.0",
|
||||||
"eslint-config-next": "15.1.6",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"postcss": "^8",
|
"@types/node": "^22.15.18",
|
||||||
"prisma": "^6.3.0",
|
"@types/pg": "^8.15.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"@types/react": "^19.1.4",
|
||||||
"typescript": "^5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"zenstack": "2.10.2"
|
"@zenstackhq/openapi": "^2.14.2",
|
||||||
},
|
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||||
"overrides": {
|
"drizzle-kit": "^0.31.1",
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"eslint": "^9.26.0",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
"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} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
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 { twx } from "@/lib/twx";
|
||||||
import {cn} from "@/lib/utils";
|
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) => [
|
export const LayoutTitle = twx.h1((props) => [cn(`text-4xl font-bold mt-5 `, props.className)]);
|
||||||
`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)=>[
|
export const LayoutDescription = twx.p((props) => [`text-lg text-muted-foreground`]);
|
||||||
cn(`text-4xl font-bold mt-5 `, props.className)
|
|
||||||
])
|
|
||||||
|
|
||||||
export const LayoutDescription = twx.p((props)=>[`text-lg text-muted-foreground`])
|
|
||||||
|
|||||||
@@ -2,56 +2,65 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
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"
|
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<
|
function AccordionItem({
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
<AccordionPrimitive.Item
|
return (
|
||||||
ref={ref}
|
<AccordionPrimitive.Item
|
||||||
className={cn("border-b", className)}
|
data-slot="accordion-item"
|
||||||
{...props}
|
className={cn("border-b last:border-b-0", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AccordionItem.displayName = "AccordionItem"
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
function AccordionTrigger({
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<AccordionPrimitive.Header className="flex">
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
<AccordionPrimitive.Trigger
|
return (
|
||||||
ref={ref}
|
<AccordionPrimitive.Header className="flex">
|
||||||
className={cn(
|
<AccordionPrimitive.Trigger
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
data-slot="accordion-trigger"
|
||||||
className
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
</AccordionPrimitive.Content>
|
||||||
</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
|
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
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 { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
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<
|
function AlertDialogOverlay({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
<AlertDialogPrimitive.Overlay
|
return (
|
||||||
className={cn(
|
<AlertDialogPrimitive.Overlay
|
||||||
"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",
|
data-slot="alert-dialog-overlay"
|
||||||
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}
|
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
)
|
||||||
))
|
}
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
function AlertDialogContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<AlertDialogPortal>
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
<AlertDialogOverlay />
|
||||||
className
|
<AlertDialogPrimitive.Content
|
||||||
)}
|
data-slot="alert-dialog-content"
|
||||||
{...props}
|
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
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="alert-dialog-header"
|
||||||
className
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
function AlertDialogFooter({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<AlertDialogPrimitive.Title
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn("text-lg font-semibold", className)}
|
data-slot="alert-dialog-footer"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
))
|
className
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
function AlertDialogTitle({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
<AlertDialogPrimitive.Description
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Title
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
data-slot="alert-dialog-title"
|
||||||
{...props}
|
className={cn("text-lg font-semibold", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AlertDialogDescription.displayName =
|
)
|
||||||
AlertDialogPrimitive.Description.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
function AlertDialogDescription({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
<AlertDialogPrimitive.Action
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Description
|
||||||
className={cn(buttonVariants(), className)}
|
data-slot="alert-dialog-description"
|
||||||
{...props}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
function AlertDialogAction({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
<AlertDialogPrimitive.Cancel
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Action
|
||||||
className={cn(
|
className={cn(buttonVariants(), className)}
|
||||||
buttonVariants({ variant: "outline" }),
|
{...props}
|
||||||
"mt-2 sm:mt-0",
|
/>
|
||||||
className
|
)
|
||||||
)}
|
}
|
||||||
{...props}
|
|
||||||
/>
|
function AlertDialogCancel({
|
||||||
))
|
className,
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
|||||||
+44
-37
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const alertVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-background text-foreground",
|
default: "bg-card text-card-foreground",
|
||||||
destructive:
|
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: {
|
defaultVariants: {
|
||||||
@@ -19,41 +19,48 @@ const alertVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
function Alert({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
variant,
|
||||||
>(({ className, variant, ...props }, ref) => (
|
...props
|
||||||
<div
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
ref={ref}
|
return (
|
||||||
role="alert"
|
<div
|
||||||
className={cn(alertVariants({ variant }), className)}
|
data-slot="alert"
|
||||||
{...props}
|
role="alert"
|
||||||
/>
|
className={cn(alertVariants({ variant }), className)}
|
||||||
))
|
{...props}
|
||||||
Alert.displayName = "Alert"
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLParagraphElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="alert-title"
|
||||||
<h5
|
className={cn(
|
||||||
ref={ref}
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AlertTitle.displayName = "AlertTitle"
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
function AlertDescription({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
data-slot="alert-description"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
))
|
className
|
||||||
AlertDescription.displayName = "AlertDescription"
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
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 }
|
export { AspectRatio }
|
||||||
|
|||||||
@@ -5,46 +5,49 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
function Avatar({
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
<AvatarPrimitive.Root
|
return (
|
||||||
ref={ref}
|
<AvatarPrimitive.Root
|
||||||
className={cn(
|
data-slot="avatar"
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
className={cn(
|
||||||
className
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
function AvatarImage({
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
<AvatarPrimitive.Image
|
return (
|
||||||
ref={ref}
|
<AvatarPrimitive.Image
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
data-slot="avatar-image"
|
||||||
{...props}
|
className={cn("aspect-square size-full", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
function AvatarFallback({
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
<AvatarPrimitive.Fallback
|
return (
|
||||||
ref={ref}
|
<AvatarPrimitive.Fallback
|
||||||
className={cn(
|
data-slot="avatar-fallback"
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
className={cn(
|
||||||
className
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
|
|||||||
+20
-10
@@ -1,20 +1,22 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
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:
|
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:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
"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",
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -23,13 +25,21 @@ const badgeVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
function Badge({
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
className,
|
||||||
VariantProps<typeof badgeVariants> {}
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return (
|
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 * as React from "react"
|
||||||
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
HTMLElement,
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||||
React.ComponentPropsWithoutRef<"nav"> & {
|
}
|
||||||
separator?: React.ReactNode
|
|
||||||
}
|
|
||||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
|
||||||
Breadcrumb.displayName = "Breadcrumb"
|
|
||||||
|
|
||||||
const BreadcrumbList = React.forwardRef<
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
HTMLOListElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"ol">
|
<ol
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="breadcrumb-list"
|
||||||
<ol
|
className={cn(
|
||||||
ref={ref}
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
className={cn(
|
className
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
BreadcrumbList.displayName = "BreadcrumbList"
|
|
||||||
|
|
||||||
const BreadcrumbItem = React.forwardRef<
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
HTMLLIElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"li">
|
<li
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="breadcrumb-item"
|
||||||
<li
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
function BreadcrumbLink({
|
||||||
HTMLAnchorElement,
|
asChild,
|
||||||
React.ComponentPropsWithoutRef<"a"> & {
|
className,
|
||||||
asChild?: boolean
|
...props
|
||||||
}
|
}: React.ComponentProps<"a"> & {
|
||||||
>(({ asChild, className, ...props }, ref) => {
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
data-slot="breadcrumb-link"
|
||||||
className={cn("transition-colors hover:text-foreground", className)}
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
|
||||||
|
|
||||||
const BreadcrumbPage = React.forwardRef<
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
HTMLSpanElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"span">
|
<span
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="breadcrumb-page"
|
||||||
<span
|
role="link"
|
||||||
ref={ref}
|
aria-disabled="true"
|
||||||
role="link"
|
aria-current="page"
|
||||||
aria-disabled="true"
|
className={cn("text-foreground font-normal", className)}
|
||||||
aria-current="page"
|
{...props}
|
||||||
className={cn("font-normal text-foreground", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({
|
function BreadcrumbSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"li">) => (
|
}: React.ComponentProps<"li">) {
|
||||||
<li
|
return (
|
||||||
role="presentation"
|
<li
|
||||||
aria-hidden="true"
|
data-slot="breadcrumb-separator"
|
||||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
role="presentation"
|
||||||
{...props}
|
aria-hidden="true"
|
||||||
>
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{children ?? <ChevronRightIcon />}
|
{...props}
|
||||||
</li>
|
>
|
||||||
)
|
{children ?? <ChevronRightIcon />}
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({
|
function BreadcrumbEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) => (
|
}: React.ComponentProps<"span">) {
|
||||||
<span
|
return (
|
||||||
role="presentation"
|
<span
|
||||||
aria-hidden="true"
|
data-slot="breadcrumb-ellipsis"
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
role="presentation"
|
||||||
{...props}
|
aria-hidden="true"
|
||||||
>
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
<DotsHorizontalIcon className="h-4 w-4" />
|
{...props}
|
||||||
<span className="sr-only">More</span>
|
>
|
||||||
</span>
|
<DotsHorizontalIcon className="size-4" />
|
||||||
)
|
<span className="sr-only">More</span>
|
||||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
|
|||||||
@@ -5,26 +5,27 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
destructive:
|
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:
|
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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
icon: "h-9 w-9",
|
icon: "size-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -34,24 +35,25 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
function Button({
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
className,
|
||||||
VariantProps<typeof buttonVariants> {
|
variant,
|
||||||
asChild?: boolean
|
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 }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@@ -1,72 +1,57 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { DayPicker } from "react-day-picker"
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
|
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({ className, classNames, showOutsideDays = true, ...props }: React.ComponentProps<typeof DayPicker>) {
|
||||||
className,
|
return (
|
||||||
classNames,
|
<DayPicker
|
||||||
showOutsideDays = true,
|
showOutsideDays={showOutsideDays}
|
||||||
...props
|
className={cn("p-3", className)}
|
||||||
}: CalendarProps) {
|
classNames={{
|
||||||
return (
|
months: "flex flex-col sm:flex-row gap-2",
|
||||||
<DayPicker
|
month: "flex flex-col gap-4",
|
||||||
showOutsideDays={showOutsideDays}
|
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||||
className={cn("p-3", className)}
|
caption_label: "text-sm font-medium",
|
||||||
classNames={{
|
nav: "flex items-center gap-1",
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
nav_button: cn(buttonVariants({ variant: "outline" }), "size-7 bg-transparent p-0 opacity-50 hover:opacity-100"),
|
||||||
month: "space-y-4",
|
nav_button_previous: "absolute left-1",
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
nav_button_next: "absolute right-1",
|
||||||
caption_label: "text-sm font-medium",
|
table: "w-full border-collapse space-x-1",
|
||||||
nav: "space-x-1 flex items-center",
|
head_row: "flex",
|
||||||
nav_button: cn(
|
head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||||
buttonVariants({ variant: "outline" }),
|
row: "flex w-full mt-2",
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
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",
|
||||||
nav_button_previous: "absolute left-1",
|
props.mode === "range"
|
||||||
nav_button_next: "absolute right-1",
|
? "[&: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"
|
||||||
table: "w-full border-collapse space-y-1",
|
: "[&:has([aria-selected])]:rounded-md"
|
||||||
head_row: "flex",
|
),
|
||||||
head_cell:
|
day: cn(buttonVariants({ variant: "ghost" }), "size-8 p-0 font-normal aria-selected:opacity-100"),
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
day_range_start: "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||||
row: "flex w-full mt-2",
|
day_range_end: "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||||
cell: cn(
|
day_selected:
|
||||||
"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",
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
props.mode === "range"
|
day_today: "bg-accent text-accent-foreground",
|
||||||
? "[&: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"
|
day_outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
),
|
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
day: cn(
|
day_hidden: "invisible",
|
||||||
buttonVariants({ variant: "ghost" }),
|
...classNames,
|
||||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
}}
|
||||||
),
|
components={{
|
||||||
day_range_start: "day-range-start",
|
IconLeft: ({ className, children: _children, ...props }) => <ChevronLeftIcon className={cn("size-4", className)} {...props} />,
|
||||||
day_range_end: "day-range-end",
|
IconRight: ({ className, children: _children, ...props }) => <ChevronRightIcon className={cn("size-4", className)} {...props} />,
|
||||||
day_selected:
|
}}
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
{...props}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Calendar.displayName = "Calendar"
|
|
||||||
|
|
||||||
export { Calendar }
|
export { Calendar };
|
||||||
|
|||||||
+82
-66
@@ -2,75 +2,91 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="card"
|
||||||
<div
|
className={cn(
|
||||||
ref={ref}
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
className={cn(
|
className
|
||||||
"rounded-xl border bg-card text-card-foreground shadow",
|
)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
Card.displayName = "Card"
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="card-header"
|
||||||
<div
|
className={cn(
|
||||||
ref={ref}
|
"@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={cn("flex flex-col space-y-1.5 p-6", className)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
CardHeader.displayName = "CardHeader"
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="card-title"
|
||||||
<div
|
className={cn("leading-none font-semibold", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
CardTitle.displayName = "CardTitle"
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="card-description"
|
||||||
<div
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
CardDescription.displayName = "CardDescription"
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="card-action"
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
className={cn(
|
||||||
))
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
CardContent.displayName = "CardContent"
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="card-content"
|
||||||
<div
|
className={cn("px-6", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
CardFooter.displayName = "CardFooter"
|
|
||||||
|
|
||||||
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"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
|
|
||||||
import useEmblaCarousel, {
|
import useEmblaCarousel, {
|
||||||
type UseEmblaCarouselType,
|
type UseEmblaCarouselType,
|
||||||
} from "embla-carousel-react"
|
} from "embla-carousel-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
type CarouselApi = UseEmblaCarouselType[1]
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||||
@@ -42,124 +41,106 @@ function useCarousel() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
const Carousel = React.forwardRef<
|
function Carousel({
|
||||||
HTMLDivElement,
|
orientation = "horizontal",
|
||||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
opts,
|
||||||
>(
|
setApi,
|
||||||
(
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
{
|
{
|
||||||
orientation = "horizontal",
|
...opts,
|
||||||
opts,
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
setApi,
|
|
||||||
plugins,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
},
|
},
|
||||||
ref
|
plugins
|
||||||
) => {
|
)
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||||
{
|
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||||
...opts,
|
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
|
||||||
},
|
|
||||||
plugins
|
|
||||||
)
|
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
if (!api) {
|
if (!api) return
|
||||||
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())
|
React.useEffect(() => {
|
||||||
setCanScrollNext(api.canScrollNext())
|
if (!api || !setApi) return
|
||||||
}, [])
|
setApi(api)
|
||||||
|
}, [api, setApi])
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
React.useEffect(() => {
|
||||||
api?.scrollPrev()
|
if (!api) return
|
||||||
}, [api])
|
onSelect(api)
|
||||||
|
api.on("reInit", onSelect)
|
||||||
|
api.on("select", onSelect)
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
return () => {
|
||||||
api?.scrollNext()
|
api?.off("select", onSelect)
|
||||||
}, [api])
|
}
|
||||||
|
}, [api, onSelect])
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
return (
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
<CarouselContext.Provider
|
||||||
if (event.key === "ArrowLeft") {
|
value={{
|
||||||
event.preventDefault()
|
carouselRef,
|
||||||
scrollPrev()
|
api: api,
|
||||||
} else if (event.key === "ArrowRight") {
|
opts,
|
||||||
event.preventDefault()
|
orientation:
|
||||||
scrollNext()
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
}
|
scrollPrev,
|
||||||
},
|
scrollNext,
|
||||||
[scrollPrev, scrollNext]
|
canScrollPrev,
|
||||||
)
|
canScrollNext,
|
||||||
|
}}
|
||||||
React.useEffect(() => {
|
>
|
||||||
if (!api || !setApi) {
|
<div
|
||||||
return
|
onKeyDownCapture={handleKeyDown}
|
||||||
}
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
setApi(api)
|
aria-roledescription="carousel"
|
||||||
}, [api, setApi])
|
data-slot="carousel"
|
||||||
|
{...props}
|
||||||
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,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
{children}
|
||||||
ref={ref}
|
</div>
|
||||||
onKeyDownCapture={handleKeyDown}
|
</CarouselContext.Provider>
|
||||||
className={cn("relative", className)}
|
)
|
||||||
role="region"
|
}
|
||||||
aria-roledescription="carousel"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CarouselContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Carousel.displayName = "Carousel"
|
|
||||||
|
|
||||||
const CarouselContent = React.forwardRef<
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { carouselRef, orientation } = useCarousel()
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={carouselRef} className="overflow-hidden">
|
<div
|
||||||
|
ref={carouselRef}
|
||||||
|
className="overflow-hidden"
|
||||||
|
data-slot="carousel-content"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
"flex",
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
@@ -169,20 +150,16 @@ const CarouselContent = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselContent.displayName = "CarouselContent"
|
|
||||||
|
|
||||||
const CarouselItem = React.forwardRef<
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { orientation } = useCarousel()
|
const { orientation } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
|
data-slot="carousel-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
@@ -191,24 +168,25 @@ const CarouselItem = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselItem.displayName = "CarouselItem"
|
|
||||||
|
|
||||||
const CarouselPrevious = React.forwardRef<
|
function CarouselPrevious({
|
||||||
HTMLButtonElement,
|
className,
|
||||||
React.ComponentProps<typeof Button>
|
variant = "outline",
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
data-slot="carousel-previous"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-8 w-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
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",
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -216,28 +194,29 @@ const CarouselPrevious = React.forwardRef<
|
|||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
<ArrowLeftIcon />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselPrevious.displayName = "CarouselPrevious"
|
|
||||||
|
|
||||||
const CarouselNext = React.forwardRef<
|
function CarouselNext({
|
||||||
HTMLButtonElement,
|
className,
|
||||||
React.ComponentProps<typeof Button>
|
variant = "outline",
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
data-slot="carousel-next"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-8 w-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
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",
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -245,12 +224,11 @@ const CarouselNext = React.forwardRef<
|
|||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowRightIcon className="h-4 w-4" />
|
<ArrowRightIcon />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
CarouselNext.displayName = "CarouselNext"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
|
|||||||
+190
-202
@@ -34,25 +34,28 @@ function useChart() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef<
|
function ChartContainer({
|
||||||
HTMLDivElement,
|
id,
|
||||||
React.ComponentProps<"div"> & {
|
className,
|
||||||
config: ChartConfig
|
children,
|
||||||
children: React.ComponentProps<
|
config,
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
...props
|
||||||
>["children"]
|
}: React.ComponentProps<"div"> & {
|
||||||
}
|
config: ChartConfig
|
||||||
>(({ id, className, children, config, ...props }, ref) => {
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"]
|
||||||
|
}) {
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId()
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
<ChartContext.Provider value={{ config }}>
|
||||||
<div
|
<div
|
||||||
|
data-slot="chart"
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -64,12 +67,11 @@ const ChartContainer = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
ChartContainer.displayName = "Chart"
|
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(
|
const colorConfig = Object.entries(config).filter(
|
||||||
([_, config]) => config.theme || config.color
|
([, config]) => config.theme || config.color
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
if (!colorConfig.length) {
|
||||||
@@ -102,219 +104,205 @@ ${colorConfig
|
|||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
function ChartTooltipContent({
|
||||||
HTMLDivElement,
|
active,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
payload,
|
||||||
React.ComponentProps<"div"> & {
|
className,
|
||||||
hideLabel?: boolean
|
indicator = "dot",
|
||||||
hideIndicator?: boolean
|
hideLabel = false,
|
||||||
indicator?: "line" | "dot" | "dashed"
|
hideIndicator = false,
|
||||||
nameKey?: string
|
label,
|
||||||
labelKey?: string
|
labelFormatter,
|
||||||
}
|
labelClassName,
|
||||||
>(
|
formatter,
|
||||||
(
|
color,
|
||||||
{
|
nameKey,
|
||||||
active,
|
labelKey,
|
||||||
payload,
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
className,
|
React.ComponentProps<"div"> & {
|
||||||
indicator = "dot",
|
hideLabel?: boolean
|
||||||
hideLabel = false,
|
hideIndicator?: boolean
|
||||||
hideIndicator = false,
|
indicator?: "line" | "dot" | "dashed"
|
||||||
label,
|
nameKey?: string
|
||||||
labelFormatter,
|
labelKey?: string
|
||||||
labelClassName,
|
}) {
|
||||||
formatter,
|
const { config } = useChart()
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
if (hideLabel || !payload?.length) {
|
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) {
|
|
||||||
return null
|
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 (
|
if (labelFormatter) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
className={cn(
|
{labelFormatter(value, payload)}
|
||||||
"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>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</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 null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||||
<div
|
}, [
|
||||||
ref={ref}
|
label,
|
||||||
className={cn(
|
labelFormatter,
|
||||||
"flex items-center justify-center gap-4",
|
payload,
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
hideLabel,
|
||||||
className
|
labelClassName,
|
||||||
)}
|
config,
|
||||||
>
|
labelKey,
|
||||||
{payload.map((item) => {
|
])
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
|
||||||
|
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 itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.dataKey}
|
||||||
className={cn(
|
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 ? (
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
<itemConfig.icon />
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
) : (
|
) : (
|
||||||
<div
|
<>
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
{itemConfig?.icon ? (
|
||||||
style={{
|
<itemConfig.icon />
|
||||||
backgroundColor: item.color,
|
) : (
|
||||||
}}
|
!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>
|
</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.
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(
|
function getPayloadConfigFromPayload(
|
||||||
|
|||||||
@@ -2,29 +2,31 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
function Checkbox({
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
<CheckboxPrimitive.Root
|
return (
|
||||||
ref={ref}
|
<CheckboxPrimitive.Root
|
||||||
className={cn(
|
data-slot="checkbox"
|
||||||
"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={cn(
|
||||||
className
|
"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}
|
)}
|
||||||
>
|
{...props}
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
className={cn("flex items-center justify-center text-current")}
|
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckboxPrimitive.Indicator
|
||||||
</CheckboxPrimitive.Indicator>
|
data-slot="checkbox-indicator"
|
||||||
</CheckboxPrimitive.Root>
|
className="flex items-center justify-center text-current transition-none"
|
||||||
))
|
>
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox }
|
||||||
|
|||||||
@@ -2,10 +2,32 @@
|
|||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
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 }
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
|
|||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user