Compare commits

...

30 commits

Author SHA1 Message Date
798467552e refactor: update labels and placeholders in login form inputs
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m1s
docker-build / docker (pull_request) Successful in 3m35s
2025-05-13 14:24:41 +02:00
dd6b82515e chore(deps): update dependency @types/react-dom to v19.1.5
All checks were successful
container-scan / Container Scan (push) Successful in 1m51s
docker-build / docker (push) Successful in 1m54s
2025-05-13 11:00:46 +00:00
f79b7b78f9 Merge pull request 'chore(deps): update dependency @types/react to v19.1.4' (#57)
All checks were successful
container-scan / Container Scan (push) Successful in 1m21s
docker-build / docker (push) Successful in 1m4s
Reviewed-on: #57
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 21:10:15 +00:00
fc828b2ac5 chore(deps): update dependency @types/react to v19.1.4
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m26s
docker-build / docker (pull_request) Successful in 3m5s
docker-build / docker (push) Successful in 4m20s
2025-05-12 20:00:50 +00:00
749c747b3a Merge pull request 'fix(deps): update dependency lucide-react to ^0.510.0' (#53)
All checks were successful
docker-build / docker (push) Successful in 1m1s
container-scan / Container Scan (push) Successful in 1m13s
Reviewed-on: #53
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 19:37:01 +00:00
b34f45230c fix(deps): update dependency lucide-react to ^0.510.0
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m15s
docker-build / docker (push) Successful in 5m47s
docker-build / docker (pull_request) Successful in 3m10s
2025-05-12 14:01:09 +00:00
04c65cd584 chore(deps): update dependency @types/react-dom to v19.1.4
All checks were successful
docker-build / docker (push) Successful in 1m2s
container-scan / Container Scan (push) Successful in 2m14s
2025-05-12 13:00:43 +00:00
86b9b9e6ad Merge pull request 'docs: update contribution guidelines to standardize branch naming and commit message format' (#52)
All checks were successful
container-scan / Container Scan (push) Successful in 1m20s
docker-build / docker (push) Successful in 3m28s
Reviewed-on: #52
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 11:22:11 +00:00
43fe60f697 Merge pull request 'feat: add settings page' (#49)
Some checks failed
container-scan / Container Scan (push) Has been cancelled
docker-build / docker (push) Has been cancelled
Reviewed-on: #49
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 11:21:41 +00:00
9aced187eb docs: update contribution guidelines to standardize branch naming and commit message format
All checks were successful
container-scan / Container Scan (pull_request) Successful in 1m13s
docker-build / docker (pull_request) Successful in 2m40s
2025-05-12 13:15:14 +02:00
90a9c5d233 style: standardize quotes and formatting in Select, Separator, and Switch components
All checks were successful
container-scan / Container Scan (pull_request) Successful in 1m30s
docker-build / docker (pull_request) Successful in 2m53s
2025-05-12 12:57:51 +02:00
d16cfbcb71 feat: added tabs to settings window and created general settings window design 2025-05-12 12:57:46 +02:00
7949c09544 feat: add ScrollableSettingsWrapper component for scrollable settings 2025-05-12 11:11:04 +02:00
f0a8275536 feat: add Radix UI Select, Separator, and Switch components 2025-05-12 11:11:04 +02:00
fd6462e02d feat: implement Settings page with tabs 2025-05-12 11:10:58 +02:00
da5a2324c6 style: standardize quotes and formatting in Tabs component 2025-05-12 11:10:11 +02:00
033801d596 feat: add Radix UI Tabs component and update dependencies 2025-05-12 10:58:26 +02:00
f631195f8a feat: add Tabs component with Radix UI integration 2025-05-12 10:56:41 +02:00
8ef6478ea4 Merge pull request 'feat: add theme toggle' (#48)
All checks were successful
container-scan / Container Scan (push) Successful in 1m9s
docker-build / docker (push) Successful in 3m51s
Reviewed-on: #48
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 08:55:24 +00:00
749a24a44d style: standardize quotes and formatting in DropdownMenu component
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m2s
docker-build / docker (pull_request) Successful in 2m57s
2025-05-12 10:01:07 +02:00
11204f0e34 feat: restructure Home component layout and add ThemePicker 2025-05-12 10:01:07 +02:00
ce39729d74 feat: add theme picker to login page layout 2025-05-12 10:00:55 +02:00
671eab15b8 feat: add dropdown menu component and theme picker 2025-05-12 09:59:31 +02:00
92af2bbb7c Merge pull request 'fix: change login page to use radix ui labels' (#47)
All checks were successful
container-scan / Container Scan (push) Successful in 1m48s
docker-build / docker (push) Successful in 3m45s
Reviewed-on: #47
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 07:55:25 +00:00
7555cb4fe8 fix: update LabeledInput component to use Label from UI library and adjust spacing in LoginForm
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m6s
docker-build / docker (pull_request) Successful in 3m14s
2025-05-12 09:44:43 +02:00
b87a761808 feat: add Radix UI label component and update dependencies 2025-05-12 09:44:43 +02:00
cd643c3c4f Merge pull request 'chore: update yarn version'
Some checks failed
container-scan / Container Scan (push) Successful in 1m9s
docker-build / docker (push) Failing after 5m34s
Reviewed-on: #50
2025-05-12 07:29:08 +00:00
e1e788b9d2 chore(ci): enable caching
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m0s
docker-build / docker (pull_request) Successful in 2m55s
2025-05-12 09:12:39 +02:00
9c87943057 chore: update yarn version 2025-05-12 09:12:37 +02:00
2ec365213e Merge pull request 'feat: create footer message'
All checks were successful
container-scan / Container Scan (push) Successful in 2m26s
docker-build / docker (push) Successful in 3m35s
Reviewed-on: #40
Reviewed-by: Dominik <mail@dominikstahl.dev>
2025-05-12 07:02:53 +00:00
21 changed files with 7797 additions and 3921 deletions

View file

@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Build an image from Dockerfile - name: Build an image from Dockerfile
run: docker build -t git.dominikstahl.dev/dhbw-we/meetup:${{ github.sha }} . run: docker buildx build -t meetup_trivy .
- name: Install Trivy - name: Install Trivy
run: | run: |
@ -23,8 +23,8 @@ 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 git.dominikstahl.dev/dhbw-we/meetup:${{ github.sha }} 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 json git.dominikstahl.dev/dhbw-we/meetup:${{ github.sha }} > trivy-report.json trivy image --exit-code 1 --severity HIGH,CRITICAL,MEDIUM --ignore-unfixed --no-progress --format json meetup_trivy > trivy-report.json
- name: Upload Trivy report - name: Upload Trivy report
uses: forgejo/upload-artifact@v4 uses: forgejo/upload-artifact@v4
@ -33,6 +33,5 @@ jobs:
- name: Clean up Docker - name: Clean up Docker
run: | run: |
docker builder prune -af --keep-storage 2GB docker buildx prune --filter=until=48h -f
docker rmi $(docker images --filter=reference="git.dominikstahl.dev/dhbw-we/meetup:*" -q) docker image rm meetup_trivy
docker image prune -f

View file

@ -45,6 +45,7 @@ jobs:
with: with:
push: true push: true
tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag}} tags: 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
@ -52,6 +53,7 @@ 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
@ -59,8 +61,9 @@ jobs:
with: with:
push: true push: true
tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag }} tags: git.dominikstahl.dev/${{ env.REPO }}:${{ steps.get-ref.outputs.tag }}
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 - name: Clean up Docker
run: | run: |
docker builder prune -af --keep-storage 2GB docker buildx prune --filter=until=48h -f
docker image prune -f

1
.yarnrc.yml Normal file
View file

@ -0,0 +1 @@
nodeLinker: node-modules

View file

@ -4,13 +4,15 @@ FROM node:22-alpine@sha256:ad1aedbcc1b0575074a91ac146d6956476c1f9985994810e4ee02
FROM base AS deps FROM base AS deps
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ RUN corepack enable
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

View file

@ -125,11 +125,22 @@ This project is built with a modern tech stack:
Contributions are welcome! If you'd like to contribute, please: Contributions are welcome! If you'd like to contribute, please:
1. Fork the repository. 1. Fork the repository.
2. Create a new branch (`git checkout -b feature/your-feature-name` or `fix/your-bug-fix`). 2. Create a new branch (`git checkout -b <action>/<issue#>-action_name`).
3. Make your changes. 3. Make your changes.
4. Commit your changes (`git commit -m 'Add some feature'`). 4. Commit your changes (`git commit -m '<action>: add some feature'`).
5. Push to the branch (`git push origin feature/your-feature-name`). 5. Push to the branch (`git push origin <action>/<issue#>-action_name`).
6. Open a Pull Request against the `main` (or `develop`) branch. 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. 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.

View file

@ -15,11 +15,18 @@
"@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",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13", "@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.509.0", "lucide-react": "^0.510.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",
@ -31,8 +38,8 @@
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@tailwindcss/postcss": "4.1.6", "@tailwindcss/postcss": "4.1.6",
"@types/node": "22.15.17", "@types/node": "22.15.17",
"@types/react": "19.1.3", "@types/react": "19.1.4",
"@types/react-dom": "19.1.3", "@types/react-dom": "19.1.5",
"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",
@ -43,5 +50,5 @@
"tw-animate-css": "1.2.9", "tw-animate-css": "1.2.9",
"typescript": "5.8.3" "typescript": "5.8.3"
}, },
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" "packageManager": "yarn@4.9.1"
} }

View file

@ -1,10 +1,16 @@
import { Logout } from '@/components/user/sso-logout-button'; import { Logout } from '@/components/user/sso-logout-button';
import { RedirectButton } from '@/components/user/redirect-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>
<Logout /> <Logout />
<RedirectButton redirectUrl='/settings' buttonText='Settings' />
</div>
</div> </div>
); );
} }

View file

@ -7,6 +7,7 @@ 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 { import {
HoverCard, HoverCard,
HoverCardTrigger, HoverCardTrigger,
@ -23,6 +24,10 @@ 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='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>
@ -38,6 +43,7 @@ export default async function LoginPage() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div>
<HoverCard> <HoverCard>
<HoverCardTrigger className='text-sm text-muted-foreground hover:underline'> <HoverCardTrigger className='text-sm text-muted-foreground hover:underline'>
<Button variant='link'>made with love</Button> <Button variant='link'>made with love</Button>

476
src/app/settings/page.tsx Normal file
View file

@ -0,0 +1,476 @@
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>
);
}

View file

@ -1,27 +1,29 @@
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-2'> <div className='flex flex-col gap-1'>
<label htmlFor={randomId}>{label}</label> <Label htmlFor={name}>{label}</Label>
<Input <Input
type={type} type={type}
placeholder={placeholder} placeholder={placeholder}
defaultValue={value} defaultValue={value}
id={randomId} id={name}
name={name}
/> />
</div> </div>
); );

View file

@ -0,0 +1,257 @@
'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,
};

View file

@ -0,0 +1,24 @@
'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 };

View file

@ -0,0 +1,185 @@
'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,
};

View file

@ -0,0 +1,28 @@
'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 };

View file

@ -0,0 +1,31 @@
'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 };

View file

@ -0,0 +1,66 @@
'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 };

View file

@ -3,16 +3,18 @@ import { Button } from '@/components/ui/button';
export default function LoginForm() { export default function LoginForm() {
return ( return (
<form className='flex flex-col gap-4 w-full'> <form className='flex flex-col gap-5 w-full'>
<LabeledInput <LabeledInput
type='email' type='email'
label='E-Mail' label='E-Mail or Username'
placeholder='Enter your E-Mail' placeholder='What you are known as.'
name='email'
/> />
<LabeledInput <LabeledInput
type='password' type='password'
label='Password' label='Password'
placeholder='Enter your Password' placeholder="Let's hope you remember it."
name='password'
/> />
<Button <Button
className='hover:bg-blue-600 hover:text-white' className='hover:bg-blue-600 hover:text-white'

View file

@ -0,0 +1,16 @@
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>
);
}

View file

@ -0,0 +1,40 @@
'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>
);
}

View file

@ -0,0 +1,16 @@
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>
);
};

10458
yarn.lock

File diff suppressed because it is too large Load diff