Compare commits
2 commits
97d9076415
...
dd35d6b956
Author | SHA1 | Date | |
---|---|---|---|
dd35d6b956 | |||
ee1f4375f8 |
33 changed files with 3828 additions and 8259 deletions
|
@ -1,9 +0,0 @@
|
||||||
DATABASE_URL="file:./dev.db"
|
|
||||||
|
|
||||||
AUTH_SECRET= # Added by `npx auth`. Read more: https://cli.authjs.dev
|
|
||||||
|
|
||||||
AUTH_AUTHENTIK_ID=
|
|
||||||
AUTH_AUTHENTIK_SECRET=
|
|
||||||
AUTH_AUTHENTIK_ISSUER=
|
|
||||||
|
|
||||||
NEXT_PUBLIC_APP_URL=
|
|
24
.github/workflows/container-scan.yml
vendored
24
.github/workflows/container-scan.yml
vendored
|
@ -9,13 +9,24 @@ jobs:
|
||||||
name: Container Scan
|
name: Container Scan
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/di0ik/forgejo_runner_container:main@sha256:672aee9a5dfc35531db3a218ad9486eb5c5d7d9ac10bdcba13110470c10403ee
|
image: node:22-bullseye@sha256:ed0338dd02fd86861a59dc1cbc2e12152f3a93c4ce5933d347d6677232000dc7
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- name: Install Docker
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ca-certificates curl
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
- name: Build an image from Dockerfile
|
- name: Build an image from Dockerfile
|
||||||
run: docker buildx build -t meetup_trivy .
|
run: docker build -t git.dominikstahl.dev/dhbw-we/meetup:${{ github.sha }} .
|
||||||
|
|
||||||
- name: Install Trivy
|
- name: Install Trivy
|
||||||
run: |
|
run: |
|
||||||
|
@ -23,15 +34,10 @@ jobs:
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
run: |
|
run: |
|
||||||
trivy image --exit-code 1 --severity HIGH,CRITICAL,MEDIUM --ignore-unfixed --no-progress --format table meetup_trivy
|
trivy image --exit-code 1 --severity HIGH,CRITICAL,MEDIUM --ignore-unfixed --no-progress --format table git.dominikstahl.dev/dhbw-we/meetup:${{ github.sha }}
|
||||||
trivy image --exit-code 1 --severity HIGH,CRITICAL,MEDIUM --ignore-unfixed --no-progress --format json meetup_trivy > trivy-report.json
|
trivy image --exit-code 1 --severity HIGH,CRITICAL,MEDIUM --ignore-unfixed --no-progress --format json git.dominikstahl.dev/dhbw-we/meetup:${{ github.sha }} > trivy-report.json
|
||||||
|
|
||||||
- name: Upload Trivy report
|
- name: Upload Trivy report
|
||||||
uses: forgejo/upload-artifact@v4
|
uses: forgejo/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: trivy-report.json
|
path: trivy-report.json
|
||||||
|
|
||||||
- name: Clean up Docker
|
|
||||||
run: |
|
|
||||||
docker buildx prune --filter=until=48h -f
|
|
||||||
docker image rm meetup_trivy
|
|
||||||
|
|
27
.github/workflows/docker-build.yml
vendored
27
.github/workflows/docker-build.yml
vendored
|
@ -12,9 +12,18 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
|
||||||
image: ghcr.io/di0ik/forgejo_runner_container:main@sha256:672aee9a5dfc35531db3a218ad9486eb5c5d7d9ac10bdcba13110470c10403ee
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install Docker
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ca-certificates curl
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
with:
|
with:
|
||||||
|
@ -37,15 +46,14 @@ jobs:
|
||||||
|
|
||||||
- name: lowercase repo name
|
- name: lowercase repo name
|
||||||
run: |
|
run: |
|
||||||
echo "REPO=$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')" >>${GITHUB_ENV}
|
echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||||
|
|
||||||
- name: Build and push (pull_request)
|
- name: Build and push (pull_request)
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag}}
|
tags: git.dominikstahl.dev/${{ env.REPO }}:sha_${{ github.sha }},git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag}}
|
||||||
cache-from: type=registry,ref=git.dominikstahl.dev/${{ env.REPO }}:buildcache
|
|
||||||
|
|
||||||
- name: Build and push (push_tag)
|
- name: Build and push (push_tag)
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||||
|
@ -53,17 +61,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag }},git.dominikstahl.dev/${{ env.REPO }}:latest
|
tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag }},git.dominikstahl.dev/${{ env.REPO }}:latest
|
||||||
cache-from: type=registry,ref=git.dominikstahl.dev/${{ env.REPO }}:buildcache
|
|
||||||
|
|
||||||
- name: Build and push (push_branch)
|
- name: Build and push (push_branch)
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||||
if: github.event_name == 'push' && github.ref_type == 'branch'
|
if: github.event_name == 'push' && github.ref_type == 'branch'
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag }}
|
tags: git.dominikstahl.dev/${{ env.REPO }}:sha_${{ github.sha }},git.dominikstahl.dev/${{ env.REPO }}:main
|
||||||
cache-from: type=registry,ref=git.dominikstahl.dev/${{ env.REPO }}:buildcache
|
|
||||||
cache-to: type=registry,ref=git.dominikstahl.dev/${{ env.REPO }}:buildcache,mode=max
|
|
||||||
|
|
||||||
- name: Clean up Docker
|
|
||||||
run: |
|
|
||||||
docker buildx prune --filter=until=48h -f
|
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -32,7 +32,6 @@ yarn-error.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
@ -40,6 +39,3 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# database
|
|
||||||
/prisma/dev.db
|
|
|
@ -1 +0,0 @@
|
||||||
nodeLinker: node-modules
|
|
|
@ -4,15 +4,13 @@ FROM node:22-alpine@sha256:ad1aedbcc1b0575074a91ac146d6956476c1f9985994810e4ee02
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN corepack enable
|
COPY package.json yarn.lock ./
|
||||||
COPY package.json yarn.lock .yarnrc.yml ./
|
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
# ----- Build -----
|
# ----- Build -----
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN corepack enable
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
@ -29,10 +27,6 @@ COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="https://git.dominikstahl.dev/DHBW-WE/MeetUp"
|
|
||||||
LABEL org.opencontainers.image.title="MeetUp"
|
|
||||||
LABEL org.opencontainers.image.description="A web application for managing meetups"
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
162
README.md
162
README.md
|
@ -1,161 +1 @@
|
||||||
# MeetUP
|
# MeetUp
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
MeetUP is a social calendar application designed to make coordinating schedules with friends seamless and intuitive. It was created because it can be a hassle coordinating meetings between multiple friends and across different friend groups. MeetUP aims to simplify the process of finding mutual availability without endless back-and-forth messaging.
|
|
||||||
|
|
||||||
## Project Status
|
|
||||||
|
|
||||||
**Still in Development:** This project is actively under development. Core features are being built, and the application is not yet feature-complete or ready for production use.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Implemented Features
|
|
||||||
|
|
||||||
- Core infrastructure setup in progress. No user-facing features are implemented yet.
|
|
||||||
|
|
||||||
### Planned Features (Roadmap)
|
|
||||||
|
|
||||||
- **Friendships:** Connect with friends to share calendars.
|
|
||||||
- **Group Calendars:** Create and manage shared calendars for groups.
|
|
||||||
- **iCal Import:** Import existing calendars from iCalendar (.ics) files.
|
|
||||||
- **iCal Export:** Export personal or shared calendars in iCalendar (.ics) format.
|
|
||||||
- **Email Notifications:** Receive email alerts for event bookings, reminders, and updates.
|
|
||||||
- **View Blocked Slots:** See when friends are busy without revealing event details.
|
|
||||||
- **Book Timeslots:** Request and confirm meeting times in friends' available slots.
|
|
||||||
- **SSO Compatibility:** Planning for Single Sign-On integration.
|
|
||||||
|
|
||||||
## Technologies Used
|
|
||||||
|
|
||||||
This project is built with a modern tech stack:
|
|
||||||
|
|
||||||
- **Package Manager:** [Yarn](https://yarnpkg.com/)
|
|
||||||
- **Framework:** [Next.js](https://nextjs.org/) - React framework for server-side rendering and static site generation.
|
|
||||||
- **Language:** [TypeScript](https://www.typescriptlang.org/) - Superset of JavaScript that adds static typing.
|
|
||||||
- **ORM:** [Prisma](https://www.prisma.io/) - Next-generation ORM for Node.js and TypeScript.
|
|
||||||
- **Authentication:** [Auth.js](https://authjs.dev/) (formerly NextAuth.js) - Authentication for Next.js.
|
|
||||||
- **Styling:** [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework.
|
|
||||||
- **UI Components:** [shadcn/ui](https://ui.shadcn.com/) - Re-usable components built using Radix UI and Tailwind CSS.
|
|
||||||
- **Containerization:** [Docker](https://www.docker.com/) (for planned self-hosting option)
|
|
||||||
- _(You can also list related tools here, e.g., ESLint, Prettier, testing libraries if you plan to use them)_
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
**Prerequisites:**
|
|
||||||
|
|
||||||
- Node.js: Version is continually upgraded. It's recommended to use the latest LTS or a recent stable version. (Check `.nvmrc` if available).
|
|
||||||
- Yarn: Version is continually upgraded. (Check `package.json` engines field if specified).
|
|
||||||
- A database supported by Prisma (e.g., PostgreSQL, MySQL, SQLite). Ensure your database server is running.
|
|
||||||
|
|
||||||
**Installation & Running Locally:**
|
|
||||||
|
|
||||||
1. **Clone the repository:**
|
|
||||||
- Using SSH:
|
|
||||||
```bash
|
|
||||||
git clone ssh://git@git.dominikstahl.dev/DHBW-WE/MeetUp.git
|
|
||||||
```
|
|
||||||
- Or using HTTPS (recommended for most users):
|
|
||||||
```bash
|
|
||||||
git clone [https://git.dominikstahl.dev/DHBW-WE/MeetUp.git](https://git.dominikstahl.dev/DHBW-WE/MeetUp.git)
|
|
||||||
```
|
|
||||||
```bash
|
|
||||||
cd MeetUp
|
|
||||||
```
|
|
||||||
2. **Install dependencies:**
|
|
||||||
```bash
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
3. **Set up environment variables:**
|
|
||||||
|
|
||||||
- You will need to create an `AUTH_SECRET`. You can generate one using the following command:
|
|
||||||
```bash
|
|
||||||
npx auth secret
|
|
||||||
```
|
|
||||||
- Copy the `.env.example` file (if it exists) to `.env.local`. If not, create `.env.local`.
|
|
||||||
```bash
|
|
||||||
# If .env.example exists:
|
|
||||||
cp .env.example .env.local
|
|
||||||
# Otherwise, create .env.local and add the following:
|
|
||||||
```
|
|
||||||
- Ensure the following environment variables are set in your `.env.local` file. Adjust `DATABASE_URL` for your specific database provider and credentials.
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Database Connection String (Prisma)
|
|
||||||
# Example for PostgreSQL: DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"
|
|
||||||
DATABASE_URL="your_database_connection_string"
|
|
||||||
|
|
||||||
# Generated with npx auth secret
|
|
||||||
AUTH_SECRET="your_generated_auth_secret"
|
|
||||||
|
|
||||||
# Authentik SSO Variables (if you are using this provider)
|
|
||||||
AUTH_AUTHENTIK_ID=
|
|
||||||
AUTH_AUTHENTIK_SECRET=
|
|
||||||
AUTH_AUTHENTIK_ISSUER=
|
|
||||||
|
|
||||||
# Base URL of your application
|
|
||||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Apply database migrations (Prisma):**
|
|
||||||
|
|
||||||
- Ensure your Prisma schema (`prisma/schema.prisma`) is defined.
|
|
||||||
- Run the following command to apply migrations and generate Prisma Client:
|
|
||||||
```bash
|
|
||||||
npx prisma migrate dev
|
|
||||||
# You might be prompted to name your first migration.
|
|
||||||
```
|
|
||||||
- (Optional: If you need to generate Prisma Client without running migrations, use `npx prisma generate`)
|
|
||||||
|
|
||||||
5. **Run the development server:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.
|
|
||||||
|
|
||||||
The test user for the application is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
email: test@example.com
|
|
||||||
password: password
|
|
||||||
```
|
|
||||||
|
|
||||||
**Self-Hosting with Docker (Planned):**
|
|
||||||
|
|
||||||
- A Docker image and `docker-compose.yml` file will be provided in the future to allow for easy self-hosting of the MeetUP application. This setup will also include database services. Instructions will be updated here once available.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! If you'd like to contribute, please:
|
|
||||||
|
|
||||||
1. Fork the repository.
|
|
||||||
2. Create a new branch (`git checkout -b <action>/<issue#>-action_name`).
|
|
||||||
3. Make your changes.
|
|
||||||
4. Commit your changes (`git commit -m '<action>: add some feature'`).
|
|
||||||
5. Push to the branch (`git push origin <action>/<issue#>-action_name`).
|
|
||||||
6. Open a Pull Request against the `main` branch.
|
|
||||||
|
|
||||||
Possible actions are:
|
|
||||||
|
|
||||||
*feat* -> Feature added
|
|
||||||
*fix* -> Fixed a bug
|
|
||||||
*test* -> Modified or added tests
|
|
||||||
*docs* -> Modified documentation
|
|
||||||
*chore* -> changes to non code files (workflows, lock files, ...)
|
|
||||||
*refactor* -> rewritten code without changing functionality
|
|
||||||
*style* -> code style (yarn format)
|
|
||||||
*revert* -> reverts a previous commit
|
|
||||||
|
|
||||||
Please ensure your code adheres to the project's coding standards (e.g., run linters/formatters if configured) and that any database schema changes are accompanied by a Prisma migration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**(Optional Sections You Might Want to Add Later):**
|
|
||||||
|
|
||||||
- **Screenshots/Demo:** (Once you have UI to show)
|
|
||||||
- **API Reference:** (If you plan to expose an API)
|
|
||||||
- **Detailed Deployment Guides:** (For various platforms beyond Docker)
|
|
||||||
- **License:** (e.g., MIT, GPL - Important for open source projects)
|
|
||||||
- **Contact:** (How to get in touch with the maintainers)
|
|
||||||
- **Acknowledgements:** (Credit to any libraries, inspirations, or contributors)
|
|
||||||
|
|
|
@ -3,9 +3,5 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: git.dominikstahl.dev/dhbw-we/meetup:main
|
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
environment:
|
|
||||||
- AUTH_SECRET=secret
|
|
||||||
- AUTH_URL=http://localhost:3000
|
|
||||||
|
|
|
@ -2,16 +2,6 @@ import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: 'img1.wikia.nocookie.net',
|
|
||||||
port: '',
|
|
||||||
pathname: '/**',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
30
package.json
30
package.json
|
@ -7,33 +7,18 @@
|
||||||
"build": "prettier --check . && next build",
|
"build": "prettier --check . && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ."
|
||||||
"prisma:migrate": "dotenv -e .env.local -- prisma migrate dev",
|
|
||||||
"prisma:generate": "dotenv -e .env.local -- prisma generate",
|
|
||||||
"prisma:studio": "dotenv -e .env.local -- prisma studio",
|
|
||||||
"prisma:db:push": "dotenv -e .env.local -- prisma db push",
|
|
||||||
"prisma:migrate:reset": "dotenv -e .env.local -- prisma migrate reset"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.9.1",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@prisma/client": "^6.7.0",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
|
||||||
"@radix-ui/react-separator": "^1.1.6",
|
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-switch": "^1.2.4",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.11",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.508.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"next-auth": "^5.0.0-beta.25",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
@ -44,19 +29,18 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "3.3.1",
|
"@eslint/eslintrc": "3.3.1",
|
||||||
"@tailwindcss/postcss": "4.1.6",
|
"@tailwindcss/postcss": "4.1.6",
|
||||||
"@types/node": "22.15.18",
|
"@types/node": "22.15.17",
|
||||||
"@types/react": "19.1.4",
|
"@types/react": "19.1.3",
|
||||||
"@types/react-dom": "19.1.5",
|
"@types/react-dom": "19.1.3",
|
||||||
"dotenv-cli": "^8.0.0",
|
|
||||||
"eslint": "9.26.0",
|
"eslint": "9.26.0",
|
||||||
"eslint-config-next": "15.3.2",
|
"eslint-config-next": "15.3.2",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"postcss": "8.5.3",
|
"postcss": "8.5.3",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"prisma": "^6.7.0",
|
"prisma": "6.7.0",
|
||||||
"tailwindcss": "4.1.6",
|
"tailwindcss": "4.1.6",
|
||||||
"tw-animate-css": "1.2.9",
|
"tw-animate-css": "1.2.9",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1"
|
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ generator client {
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,12 +102,12 @@ model Account {
|
||||||
type String
|
type String
|
||||||
provider String
|
provider String
|
||||||
providerAccountId String @map("provider_account_id")
|
providerAccountId String @map("provider_account_id")
|
||||||
refresh_token String?
|
refresh_token String? @db.Text
|
||||||
access_token String?
|
access_token String? @db.Text
|
||||||
expires_at Int?
|
expires_at Int?
|
||||||
token_type String?
|
token_type String?
|
||||||
scope String?
|
scope String?
|
||||||
id_token String?
|
id_token String? @db.Text
|
||||||
session_state String?
|
session_state String?
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
import { RedirectButton } from '@/components/user/redirect-button';
|
import { Logout } from '@/components/user/sso-logout-button';
|
||||||
import { ThemePicker } from '@/components/user/theme-picker';
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center h-screen'>
|
|
||||||
<div className='absolute top-4 right-4'>{<ThemePicker />}</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<RedirectButton redirectUrl='/logout' buttonText='Logout' />
|
<Logout />
|
||||||
<RedirectButton redirectUrl='/settings' buttonText='Settings' />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
import { auth, providerMap } from '@/auth';
|
import { auth } from '@/auth';
|
||||||
import SSOLogin from '@/components/user/sso-login-button';
|
import SSOLogin from '@/components/user/sso-login-button';
|
||||||
import LoginForm from '@/components/user/login-form';
|
import LoginForm from '@/components/user/login-form';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import '@/app/globals.css';
|
import '@/app/globals.css';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { ThemePicker } from '@/components/user/theme-picker';
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardTrigger,
|
|
||||||
HoverCardContent,
|
|
||||||
} from '@/components/ui/hover-card';
|
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
@ -23,11 +15,6 @@ export default async function LoginPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center h-screen'>
|
<div className='flex flex-col items-center justify-center h-screen'>
|
||||||
<div className='flex flex-col items-center justify-center h-screen'>
|
|
||||||
<div className='absolute top-4 right-4'>
|
|
||||||
<ThemePicker />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Card className='w-[350px] max-w-screen'>
|
<Card className='w-[350px] max-w-screen'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='text-lg text-center'>Login</CardTitle>
|
<CardTitle className='text-lg text-center'>Login</CardTitle>
|
||||||
|
@ -35,32 +22,13 @@ export default async function LoginPage() {
|
||||||
<CardContent className='gap-6 flex flex-col'>
|
<CardContent className='gap-6 flex flex-col'>
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
|
|
||||||
{providerMap.length > 0 && <hr />}
|
<hr />
|
||||||
|
|
||||||
{providerMap.map((provider) => (
|
{process.env.AUTH_AUTHENTIK_ISSUER && (
|
||||||
<SSOLogin
|
<SSOLogin provider='authentik' providerDisplayName='SSO' />
|
||||||
key={provider.id}
|
)}
|
||||||
provider={provider.id}
|
|
||||||
providerDisplayName={provider.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger className='text-sm text-muted-foreground hover:underline'>
|
|
||||||
<Button variant='link'>made with love</Button>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className='flex items-center justify-center'>
|
|
||||||
<Image
|
|
||||||
src='https://img1.wikia.nocookie.net/__cb20140808110649/clubpenguin/images/a/a1/Action_Dance_Light_Blue.gif'
|
|
||||||
width='150'
|
|
||||||
height='150'
|
|
||||||
alt='dancing penguin'
|
|
||||||
></Image>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { signOut } from '@/auth';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function SignOutPage() {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center justify-center h-screen'>
|
|
||||||
<form
|
|
||||||
action={async () => {
|
|
||||||
'use server';
|
|
||||||
await signOut({ redirectTo: '/login' });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card className='w-[350px] max-w-screen'>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className='text-lg text-center'>Logout</CardTitle>
|
|
||||||
<CardDescription className='text-center'>
|
|
||||||
Are you sure you want to log out?
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='gap-6 flex flex-col'>
|
|
||||||
<Button
|
|
||||||
className='hover:bg-blue-600 hover:text-white'
|
|
||||||
type='submit'
|
|
||||||
variant='secondary'
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,3 @@
|
||||||
import { auth } from '@/auth';
|
export default function Home() {
|
||||||
import { redirect } from 'next/navigation';
|
return <div></div>;
|
||||||
|
|
||||||
export default async function Home() {
|
|
||||||
const session = await auth();
|
|
||||||
|
|
||||||
if (!session?.user) redirect('/login');
|
|
||||||
else redirect('/home');
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,476 +0,0 @@
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
|
||||||
return (
|
|
||||||
<div className='fixed inset-0 flex items-center justify-center p-4 bg-background/50 backdrop-blur-sm'>
|
|
||||||
<div className='rounded-lg border bg-card text-card-foreground shadow-xl max-w-[700px] w-full h-auto max-h-[calc(100vh-2rem)] flex flex-col'>
|
|
||||||
<Tabs
|
|
||||||
defaultValue='general'
|
|
||||||
className='w-full flex flex-col flex-grow min-h-0'
|
|
||||||
>
|
|
||||||
<TabsList className='grid w-full grid-cols-3 sm:grid-cols-5'>
|
|
||||||
<TabsTrigger value='general'>Account</TabsTrigger>
|
|
||||||
<TabsTrigger value='notifications'>Notifications</TabsTrigger>
|
|
||||||
<TabsTrigger value='calendarAvailability'>Calendar</TabsTrigger>
|
|
||||||
<TabsTrigger value='sharingPrivacy'>Privacy</TabsTrigger>
|
|
||||||
<TabsTrigger value='appearance'>Appearance</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value='general' className='flex-grow overflow-hidden'>
|
|
||||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
|
||||||
<ScrollableSettingsWrapper>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Account Settings</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Manage your account details and preferences.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-6'>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='displayName'>Display Name</Label>
|
|
||||||
<Input id='displayName' placeholder='Your Name' />
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='email'>Email Address</Label>
|
|
||||||
<Input
|
|
||||||
id='email'
|
|
||||||
type='email'
|
|
||||||
placeholder='your.email@example.com'
|
|
||||||
readOnly
|
|
||||||
value='user-email@example.com'
|
|
||||||
/>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Email is managed by your SSO provider.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='profilePicture'>Profile Picture</Label>
|
|
||||||
<Input id='profilePicture' type='file' />
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Upload a new profile picture.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='timezone'>Timezone</Label>
|
|
||||||
<Input id='displayName' placeholder='Europe/Berlin' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='language'>Language</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='language'>
|
|
||||||
<SelectValue placeholder='Select language' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='en'>English</SelectItem>
|
|
||||||
<SelectItem value='de'>German</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='pt-4'>
|
|
||||||
<Button variant='destructive'>Delete Account</Button>
|
|
||||||
<p className='text-sm text-muted-foreground pt-1'>
|
|
||||||
Permanently delete your account and all associated data.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</ScrollableSettingsWrapper>
|
|
||||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
|
||||||
<Button variant='secondary'>Exit</Button>
|
|
||||||
<Button>Save Changes</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent
|
|
||||||
value='notifications'
|
|
||||||
className='flex-grow overflow-hidden'
|
|
||||||
>
|
|
||||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
|
||||||
<ScrollableSettingsWrapper>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Notification Preferences</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Choose how you want to be notified.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-6'>
|
|
||||||
<div className='flex items-center justify-between space-x-2 p-3 rounded-md border'>
|
|
||||||
<Label
|
|
||||||
htmlFor='masterEmailNotifications'
|
|
||||||
className='font-normal'
|
|
||||||
>
|
|
||||||
Enable All Email Notifications
|
|
||||||
</Label>
|
|
||||||
<Switch id='masterEmailNotifications' />
|
|
||||||
</div>
|
|
||||||
<div className='space-y-4 pl-2 border-l-2 ml-2'>
|
|
||||||
<div className='flex items-center justify-between space-x-2'>
|
|
||||||
<Label
|
|
||||||
htmlFor='newMeetingBookings'
|
|
||||||
className='font-normal'
|
|
||||||
>
|
|
||||||
New Meeting Bookings
|
|
||||||
</Label>
|
|
||||||
<Switch id='newMeetingBookings' />
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between space-x-2'>
|
|
||||||
<Label
|
|
||||||
htmlFor='meetingConfirmations'
|
|
||||||
className='font-normal'
|
|
||||||
>
|
|
||||||
Meeting Confirmations/Cancellations
|
|
||||||
</Label>
|
|
||||||
<Switch id='meetingConfirmations' />
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between space-x-2'>
|
|
||||||
<Label
|
|
||||||
htmlFor='enableMeetingReminders'
|
|
||||||
className='font-normal'
|
|
||||||
>
|
|
||||||
Meeting Reminders
|
|
||||||
</Label>
|
|
||||||
<Switch id='enableMeetingReminders' />
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2 pl-6'>
|
|
||||||
<Label htmlFor='remindBefore'>Remind me before</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='remindBefore'>
|
|
||||||
<SelectValue placeholder='Select reminder time' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='15m'>15 minutes</SelectItem>
|
|
||||||
<SelectItem value='30m'>30 minutes</SelectItem>
|
|
||||||
<SelectItem value='1h'>1 hour</SelectItem>
|
|
||||||
<SelectItem value='1d'>1 day</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between space-x-2'>
|
|
||||||
<Label htmlFor='friendRequests' className='font-normal'>
|
|
||||||
Friend Requests
|
|
||||||
</Label>
|
|
||||||
<Switch id='friendRequests' />
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between space-x-2'>
|
|
||||||
<Label htmlFor='groupUpdates' className='font-normal'>
|
|
||||||
Group Invitations/Updates
|
|
||||||
</Label>
|
|
||||||
<Switch id='groupUpdates' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</ScrollableSettingsWrapper>
|
|
||||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
|
||||||
<Button variant='secondary'>Exit</Button>
|
|
||||||
<Button>Save Changes</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent
|
|
||||||
value='calendarAvailability'
|
|
||||||
className='flex-grow overflow-hidden'
|
|
||||||
>
|
|
||||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
|
||||||
<ScrollableSettingsWrapper>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Calendar & Availability</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Manage your calendar display, default availability, and iCal
|
|
||||||
integrations.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-6'>
|
|
||||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
|
||||||
<legend className='text-sm font-medium px-1'>
|
|
||||||
Display
|
|
||||||
</legend>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='defaultCalendarView'>
|
|
||||||
Default Calendar View
|
|
||||||
</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='defaultCalendarView'>
|
|
||||||
<SelectValue placeholder='Select view' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='day'>Day</SelectItem>
|
|
||||||
<SelectItem value='week'>Week</SelectItem>
|
|
||||||
<SelectItem value='month'>Month</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='weekStartsOn'>Week Starts On</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='weekStartsOn'>
|
|
||||||
<SelectValue placeholder='Select day' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='sunday'>Sunday</SelectItem>
|
|
||||||
<SelectItem value='monday'>Monday</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between space-x-2'>
|
|
||||||
<Label htmlFor='showWeekends' className='font-normal'>
|
|
||||||
Show Weekends
|
|
||||||
</Label>
|
|
||||||
<Switch id='showWeekends' defaultChecked />
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
|
||||||
<legend className='text-sm font-medium px-1'>
|
|
||||||
Availability
|
|
||||||
</legend>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label>Working Hours</Label>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Define your typical available hours (e.g.,
|
|
||||||
Monday-Friday, 9 AM - 5 PM).
|
|
||||||
</p>
|
|
||||||
<Button variant='outline' size='sm'>
|
|
||||||
Set Working Hours
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='minNoticeBooking'>
|
|
||||||
Minimum Notice for Bookings
|
|
||||||
</Label>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Min time before a booking can be made.
|
|
||||||
</p>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Input
|
|
||||||
id='bookingWindow'
|
|
||||||
type='text'
|
|
||||||
placeholder='e.g., 1h'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='bookingWindow'>
|
|
||||||
Booking Window (days in advance)
|
|
||||||
</Label>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Max time in advance a booking can be made.
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
id='bookingWindow'
|
|
||||||
type='number'
|
|
||||||
placeholder='e.g., 30d'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
|
||||||
<legend className='text-sm font-medium px-1'>
|
|
||||||
iCalendar Integration
|
|
||||||
</legend>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='icalImport'>Import iCal Feed URL</Label>
|
|
||||||
<Input
|
|
||||||
id='icalImport'
|
|
||||||
type='url'
|
|
||||||
placeholder='https://calendar.example.com/feed.ics'
|
|
||||||
/>
|
|
||||||
<Button size='sm' className='mt-1'>
|
|
||||||
Add Feed
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label>Export Your Calendar</Label>
|
|
||||||
<Button variant='outline' size='sm'>
|
|
||||||
Get iCal Export URL
|
|
||||||
</Button>
|
|
||||||
<Button variant='outline' size='sm' className='ml-2'>
|
|
||||||
Download .ics File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</CardContent>
|
|
||||||
</ScrollableSettingsWrapper>
|
|
||||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
|
||||||
<Button variant='secondary'>Exit</Button>
|
|
||||||
<Button>Save Changes</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent
|
|
||||||
value='sharingPrivacy'
|
|
||||||
className='flex-grow overflow-hidden'
|
|
||||||
>
|
|
||||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
|
||||||
<ScrollableSettingsWrapper>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Sharing & Privacy</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Control who can see your calendar and book time with you.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-6'>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='defaultVisibility'>
|
|
||||||
Default Calendar Visibility
|
|
||||||
</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='defaultVisibility'>
|
|
||||||
<SelectValue placeholder='Select visibility' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='private'>
|
|
||||||
Private (Only You)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value='freebusy'>
|
|
||||||
Free/Busy for Friends
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value='fulldetails'>
|
|
||||||
Full Details for Friends
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='whoCanSeeFull'>
|
|
||||||
Who Can See Your Full Calendar Details?
|
|
||||||
</Label>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
(Override for Default Visibility)
|
|
||||||
<br />
|
|
||||||
<span className='text-sm text-muted-foreground'>
|
|
||||||
This setting will override the default visibility for
|
|
||||||
your calendar. You can set specific friends or groups to
|
|
||||||
see your full calendar details.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='whoCanSeeFull'>
|
|
||||||
<SelectValue placeholder='Select audience' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='me'>Only Me</SelectItem>
|
|
||||||
<SelectItem value='friends'>My Friends</SelectItem>
|
|
||||||
<SelectItem value='specific'>
|
|
||||||
Specific Friends/Groups (manage separately)
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='whoCanBook'>
|
|
||||||
Who Can Book Time With You?
|
|
||||||
</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='whoCanBook'>
|
|
||||||
<SelectValue placeholder='Select audience' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='none'>No One</SelectItem>
|
|
||||||
<SelectItem value='friends'>My Friends</SelectItem>
|
|
||||||
<SelectItem value='specific'>
|
|
||||||
Specific Friends/Groups (manage separately)
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label>Blocked Users</Label>
|
|
||||||
<Button variant='outline'>Manage Blocked Users</Button>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Prevent specific users from seeing your calendar or
|
|
||||||
booking time.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</ScrollableSettingsWrapper>
|
|
||||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
|
||||||
<Button variant='secondary'>Exit</Button>
|
|
||||||
<Button>Save Changes</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value='appearance' className='flex-grow overflow-hidden'>
|
|
||||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
|
||||||
<ScrollableSettingsWrapper>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Appearance</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Customize the look and feel of the application.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-6'>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='theme'>Theme</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='theme'>
|
|
||||||
<SelectValue placeholder='Select theme' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='light'>Light</SelectItem>
|
|
||||||
<SelectItem value='dark'>Dark</SelectItem>
|
|
||||||
<SelectItem value='system'>System Default</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='dateFormat'>Date Format</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='dateFormat'>
|
|
||||||
<SelectValue placeholder='Select date format' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='ddmmyyyy'>DD/MM/YYYY</SelectItem>
|
|
||||||
<SelectItem value='mmddyyyy'>MM/DD/YYYY</SelectItem>
|
|
||||||
<SelectItem value='yyyymmdd'>YYYY-MM-DD</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<Label htmlFor='timeFormat'>Time Format</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id='timeFormat'>
|
|
||||||
<SelectValue placeholder='Select time format' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='24h'>24-hour</SelectItem>
|
|
||||||
<SelectItem value='12h'>12-hour</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</ScrollableSettingsWrapper>
|
|
||||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
|
||||||
<Button variant='secondary'>Exit</Button>
|
|
||||||
<Button>Save Changes</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
50
src/auth.ts
50
src/auth.ts
|
@ -1,53 +1,13 @@
|
||||||
import NextAuth from 'next-auth';
|
import NextAuth from 'next-auth';
|
||||||
|
|
||||||
import type { Provider } from 'next-auth/providers';
|
|
||||||
import Credentials from 'next-auth/providers/credentials';
|
|
||||||
|
|
||||||
import Authentik from 'next-auth/providers/authentik';
|
import Authentik from 'next-auth/providers/authentik';
|
||||||
|
|
||||||
import { PrismaAdapter } from '@auth/prisma-adapter';
|
|
||||||
import { prisma } from '@/prisma';
|
|
||||||
|
|
||||||
const providers: Provider[] = [
|
|
||||||
!process.env.DISABLE_PASSWORD_LOGIN &&
|
|
||||||
Credentials({
|
|
||||||
credentials: { password: { label: 'Password', type: 'password' } },
|
|
||||||
authorize(c) {
|
|
||||||
if (c.password !== 'password') return null;
|
|
||||||
return {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test User',
|
|
||||||
email: 'test@example.com',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
process.env.AUTH_AUTHENTIK_ID && Authentik,
|
|
||||||
].filter(Boolean) as Provider[];
|
|
||||||
|
|
||||||
export const providerMap = providers
|
|
||||||
.map((provider) => {
|
|
||||||
if (typeof provider === 'function') {
|
|
||||||
const providerData = provider();
|
|
||||||
return { id: providerData.id, name: providerData.name };
|
|
||||||
} else {
|
|
||||||
return { id: provider.id, name: provider.name };
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((provider) => provider.id !== 'credentials');
|
|
||||||
|
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
providers: [],
|
providers: [process.env.AUTH_AUTHENTIK_ISSUER ? Authentik : null].filter(
|
||||||
adapter: PrismaAdapter(prisma),
|
(x) => x !== null,
|
||||||
session: {
|
),
|
||||||
strategy: 'jwt',
|
|
||||||
},
|
|
||||||
pages: {
|
|
||||||
signIn: '/login',
|
|
||||||
signOut: '/logout',
|
|
||||||
},
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
authorized({ auth }) {
|
authorized: async ({ auth }) => {
|
||||||
return !!auth?.user;
|
return !!auth;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,29 +1,27 @@
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
|
|
||||||
export default function LabeledInput({
|
export default function LabeledInput({
|
||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
value,
|
value,
|
||||||
name,
|
|
||||||
}: {
|
}: {
|
||||||
type: 'text' | 'email' | 'password';
|
type: 'text' | 'email' | 'password';
|
||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
name?: string;
|
|
||||||
}) {
|
}) {
|
||||||
|
const randomId = Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-2'>
|
||||||
<Label htmlFor={name}>{label}</Label>
|
<label htmlFor={randomId}>{label}</label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
id={name}
|
id={randomId}
|
||||||
name={name}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,257 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function DropdownMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
||||||
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Trigger
|
|
||||||
data-slot='dropdown-menu-trigger'
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
data-slot='dropdown-menu-content'
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = 'default',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean;
|
|
||||||
variant?: 'default' | 'destructive';
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
data-slot='dropdown-menu-item'
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
data-slot='dropdown-menu-checkbox-item'
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className='size-4' />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
data-slot='dropdown-menu-radio-group'
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
data-slot='dropdown-menu-radio-item'
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className='size-2 fill-current' />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
data-slot='dropdown-menu-label'
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
data-slot='dropdown-menu-separator'
|
|
||||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'span'>) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot='dropdown-menu-shortcut'
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
||||||
return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
data-slot='dropdown-menu-sub-trigger'
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className='ml-auto size-4' />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
data-slot='dropdown-menu-sub-content'
|
|
||||||
className={cn(
|
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function HoverCard({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
||||||
return <HoverCardPrimitive.Root data-slot='hover-card' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function HoverCardTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HoverCardContent({
|
|
||||||
className,
|
|
||||||
align = 'center',
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<HoverCardPrimitive.Portal data-slot='hover-card-portal'>
|
|
||||||
<HoverCardPrimitive.Content
|
|
||||||
data-slot='hover-card-content'
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</HoverCardPrimitive.Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
|
@ -1,24 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot='label'
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label };
|
|
|
@ -1,185 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Select({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
||||||
return <SelectPrimitive.Root data-slot='select' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
||||||
return <SelectPrimitive.Group data-slot='select-group' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
||||||
return <SelectPrimitive.Value data-slot='select-value' {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = 'default',
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
||||||
size?: 'sm' | 'default';
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
data-slot='select-trigger'
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDownIcon className='size-4 opacity-50' />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
position = 'popper',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
data-slot='select-content'
|
|
||||||
className={cn(
|
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
|
||||||
position === 'popper' &&
|
|
||||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
'p-1',
|
|
||||||
position === 'popper' &&
|
|
||||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
data-slot='select-label'
|
|
||||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
data-slot='select-item'
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className='absolute right-2 flex size-3.5 items-center justify-center'>
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className='size-4' />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
data-slot='select-separator'
|
|
||||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
data-slot='select-scroll-up-button'
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-default items-center justify-center py-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon className='size-4' />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
data-slot='select-scroll-down-button'
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-default items-center justify-center py-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon className='size-4' />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Separator({
|
|
||||||
className,
|
|
||||||
orientation = 'horizontal',
|
|
||||||
decorative = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SeparatorPrimitive.Root
|
|
||||||
data-slot='separator-root'
|
|
||||||
decorative={decorative}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Separator };
|
|
|
@ -1,31 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Switch({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SwitchPrimitive.Root
|
|
||||||
data-slot='switch'
|
|
||||||
className={cn(
|
|
||||||
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SwitchPrimitive.Thumb
|
|
||||||
data-slot='switch-thumb'
|
|
||||||
className={cn(
|
|
||||||
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SwitchPrimitive.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Switch };
|
|
|
@ -1,66 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Tabs({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot='tabs'
|
|
||||||
className={cn('flex flex-col gap-2', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
data-slot='tabs-list'
|
|
||||||
className={cn(
|
|
||||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
data-slot='tabs-trigger'
|
|
||||||
className={cn(
|
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot='tabs-content'
|
|
||||||
className={cn('flex-1 outline-none', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
|
@ -1,38 +1,18 @@
|
||||||
import { signIn } from '@/auth';
|
|
||||||
import LabeledInput from '@/components/labeled-input';
|
import LabeledInput from '@/components/labeled-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AuthError } from 'next-auth';
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
const SIGNIN_ERROR_URL = '/error';
|
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
return (
|
return (
|
||||||
<form
|
<form className='flex flex-col gap-4 w-full'>
|
||||||
className='flex flex-col gap-5 w-full'
|
|
||||||
action={async (formData) => {
|
|
||||||
'use server';
|
|
||||||
try {
|
|
||||||
await signIn('credentials', formData);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AuthError) {
|
|
||||||
return redirect(`${SIGNIN_ERROR_URL}?error=${error.type}`);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
type='email'
|
type='email'
|
||||||
label='E-Mail or Username'
|
label='E-Mail'
|
||||||
placeholder='What you are known as.'
|
placeholder='Enter your E-Mail'
|
||||||
name='email'
|
|
||||||
/>
|
/>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
type='password'
|
type='password'
|
||||||
label='Password'
|
label='Password'
|
||||||
placeholder="Let's hope you remember it."
|
placeholder='Enter your Password'
|
||||||
name='password'
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className='hover:bg-blue-600 hover:text-white'
|
className='hover:bg-blue-600 hover:text-white'
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export function RedirectButton({
|
|
||||||
redirectUrl,
|
|
||||||
buttonText,
|
|
||||||
}: {
|
|
||||||
redirectUrl: string;
|
|
||||||
buttonText: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Link href={redirectUrl}>
|
|
||||||
<Button>{buttonText}</Button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
18
src/components/user/sso-logout-button.tsx
Normal file
18
src/components/user/sso-logout-button.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { signOut } from '@/auth';
|
||||||
|
import { IconButton } from '@/components/icon-button';
|
||||||
|
import { faDoorOpen } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
export function Logout() {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
action={async () => {
|
||||||
|
'use server';
|
||||||
|
await signOut({ redirectTo: '/login' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton type='submit' variant='destructive' icon={faDoorOpen}>
|
||||||
|
Sign Out
|
||||||
|
</IconButton>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { Moon, Sun } from 'lucide-react';
|
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
|
|
||||||
export function ThemePicker() {
|
|
||||||
const { setTheme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant='outline' size='icon'>
|
|
||||||
<Sun className='h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
|
|
||||||
<Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
|
|
||||||
<span className='sr-only'>Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align='end'>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
|
||||||
Light
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
|
||||||
Dark
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
||||||
System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface ScrollableContentWrapperProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ScrollableSettingsWrapper: React.FC<
|
|
||||||
ScrollableContentWrapperProps
|
|
||||||
> = ({ children, className = '' }) => {
|
|
||||||
return (
|
|
||||||
<div className={`h-[500px] overflow-y-auto space-y-2 ${className}`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,4 +1,15 @@
|
||||||
export { auth as middleware } from '@/auth';
|
import { auth } from '@/auth';
|
||||||
|
|
||||||
|
export default auth((req) => {
|
||||||
|
if (
|
||||||
|
!req.auth &&
|
||||||
|
req.nextUrl.pathname !== '/login' &&
|
||||||
|
process.env.MEETUP_SKIP_LOGIN !== 'true'
|
||||||
|
) {
|
||||||
|
const newUrl = new URL('/login', req.nextUrl.origin);
|
||||||
|
return Response.redirect(newUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { PrismaClient } from "@prisma/client"
|
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
|
|
||||||
|
|
||||||
export const prisma = globalForPrisma.prisma || new PrismaClient()
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
|
|
Loading…
Add table
Add a link
Reference in a new issue