Merge pull request #197 from bartvdbraak/refactor/svelte-rewrite

Refactor and add features to SvelteKit project
This commit is contained in:
Bart van der Braak 2024-02-16 08:50:27 +01:00 committed by GitHub
commit 2bd4f9d60c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
275 changed files with 8700 additions and 6404 deletions

View file

@ -1,2 +0,0 @@
ignores: ["eslint", "babel-*", "depcheck", "@types/node", "@types/react-dom", "autoprefixer", "postcss"]
skip-missing: false

View file

@ -1,10 +0,0 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View file

@ -1,2 +1,2 @@
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
SERVER_PB="http://localhost:8090"
PUBLIC_CLIENT_PB="http://127.0.0.1:8090"

View file

@ -1 +1,13 @@
.github
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

37
.eslintrc.cjs Normal file
View file

@ -0,0 +1,37 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
],
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{ varsIgnorePattern: '^\\$\\$(Props|Events|Slots)$' }
]
}
};

View file

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

5
.github/renovate.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"reviewers": ["bartvdbraak"]
}

View file

@ -1,34 +0,0 @@
name: Cleanup GitHub Pages on Branch Deletion
on:
delete
permissions:
contents: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.5.3
- name: Delete directory in gh-pages
if: github.event.ref_type == 'branch'
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git fetch --all
git checkout gh-pages
branchName=$(echo "${{ github.event.ref }}" | sed -e 's,^refs/heads/,,')
if [ -d "./${branchName}" ]; then
git rm -rf --ignore-unmatch "${branchName}"
git commit -m "Cleanup directory for deleted branch ${branchName}"
git push origin gh-pages
else
echo "Directory doesn't exist for branch ${branchName}"
fi

View file

@ -1,152 +0,0 @@
name: Lighthouse report
on:
pull_request:
permissions:
contents: write
pages: write
id-token: write
pull-requests: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
unlighthouse:
environment:
name: github-pages
url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ env.BRANCH_NAME }}
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18]
steps:
- name: Sticky Comment on Pull Request
uses: marocchino/sticky-pull-request-comment@v2.7.0
with:
header: unlighthouse
message: |
⚡️ Lighthouse report
![Loading](https://github.com/bartvdbraak/omnidash/assets/3996360/8e85bc78-53ac-41de-bdb6-bedfe8c6d8c1)
- name: Checkout Git repository
uses: actions/checkout@v3.5.3
- name: Setup pnpm
uses: pnpm/action-setup@v2.4.0
with:
version: 8.6.2
- name: Setup Node.js
uses: actions/setup-node@v3.7.0
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install Dependencies
run: pnpm add -g @unlighthouse/cli puppeteer
- name: Retrieve Vercel Preview URL
uses: zentered/vercel-preview-url@v1.1.9
id: vercel_preview_url
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
with:
vercel_project_id: ${{ vars.VERCEL_PROJECT_ID }}
- name: Await Vercel Deployment
uses: UnlyEd/github-action-await-vercel@v1.2.43
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
with:
deployment-url: ${{ steps.vercel_preview_url.outputs.preview_url }}
timeout: 120
- name: Build Unlighthouse report
run: |
unlighthouse-ci \
--router-prefix "${{ github.event.repository.name }}/${{ env.BRANCH_NAME }}" \
--site "${{ steps.vercel_preview_url.outputs.preview_url }}" \
--reporter jsonExpanded \
--build-static
- name: Deploy report to GitHub pages
uses: peaceiris/actions-gh-pages@v3.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./.unlighthouse
destination_dir: ${{ env.BRANCH_NAME }}
- name: Format lighthouse score
id: format_lighthouse_score
uses: actions/github-script@v6.4.1
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
const result = JSON.parse(fs.readFileSync('.unlighthouse/ci-result.json', 'utf8'));
const formatScore = score => `${Math.round(score * 100)} (${score})`;
const getEmoji = score => score >= 0.9 ? '🟢' : score >= 0.5 ? '🟠' : '🔴';
const score = res => `${getEmoji(res)} ${formatScore(res)}`;
const reportUrl = `https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ env.BRANCH_NAME }}`;
const comment = [
`⚡️ Lighthouse report for the changes in this PR:`,
'| Category | Score |',
'| --- | --- |',
`| Performance | ${score(result.summary.categories.performance.averageScore)} |`,
`| Accessibility | ${score(result.summary.categories.accessibility.averageScore)} |`,
`| Best practices | ${score(result.summary.categories['best-practices'].averageScore)} |`,
`| SEO | ${score(result.summary.categories.seo.averageScore)} |`,
`| *Overall* | ${score(result.summary.score)} |`,
'',
'*Lighthouse scores for individual routes:*',
'',
'| Path | Performance | Accessibility | Best practices | SEO | Overall |',
'| --- | --- | --- | --- | --- | --- |',
`${result.routes.map(route => `| ${route.path} | ${score(route.categories.performance.score)} | ${score(route.categories.accessibility.score)} | ${score(route.categories['best-practices'].score)} | ${score(route.categories.seo.score)} | ${score(route.score)} |`).join('\n')}`,
'',
'*Lighthouse metrics:*',
'',
'| Metric | Average Value |',
'| --- | --- |',
`${Object.entries(result.summary.metrics).map(([metric, { averageNumericValue }]) => `| ${metric} | ${averageNumericValue} |`).join('\n')}`,
'',
`View the full Lighthouse report [here](${reportUrl}).`,
'',
'Learn more about the Lighthouse metrics:',
'- [Largest Contentful Paint](https://web.dev/lighthouse-largest-contentful-paint/)',
'- [Cumulative Layout Shift](https://web.dev/cls/)',
'- [First Contentful Paint](https://web.dev/first-contentful-paint/)',
'- [Total Blocking Time](https://web.dev/lighthouse-total-blocking-time/)',
'- [Max Potential First Input Delay](https://web.dev/lighthouse-max-potential-fid/)',
'- [Time to Interactive](https://web.dev/interactive/)',
].join('\n');
core.setOutput("comment", comment);
- name: Sticky Comment on Pull Request with result
uses: marocchino/sticky-pull-request-comment@v2.7.0
with:
header: unlighthouse
message: |
${{ steps.format_lighthouse_score.outputs.comment }}
- name: Sticky Comment on Pull Request with failure
uses: marocchino/sticky-pull-request-comment@v2.7.0
if: ${{ failure() }}
with:
header: unlighthouse
message: |
⚡️ Lighthouse report failed
See deployment for any errors

View file

@ -1,12 +1,6 @@
name: Linting and Dependency Check
name: Linting
on:
push:
branches:
- main
pull_request:
branches:
- main
on: [pull_request]
permissions:
checks: write
@ -18,18 +12,18 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
node-version: [18]
steps:
- name: Checkout Git repository
uses: actions/checkout@v3.5.3
uses: actions/checkout@v4.1.1
- name: Setup pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v3.0.0
with:
version: 8.6.2
version: latest
- name: Setup Node.js
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
@ -42,6 +36,3 @@ jobs:
with:
eslint: true
prettier: true
- name: Run dependency check
run: npx depcheck

170
.github/workflows/unlighthouse.yaml vendored Normal file
View file

@ -0,0 +1,170 @@
name: Unlighthouse
on:
push:
branches: main
pull_request:
permissions:
pull-requests: write
jobs:
unlighthouse:
runs-on: ubuntu-latest
env:
COMMENT_ID: unlighthouse-node${{matrix.node-version}}
PORT: 8000
CLOUDFLARE_PROJECT: hellobart-unlighthouse
strategy:
matrix:
node-version: [20]
steps:
- name: Create initial comment
uses: marocchino/sticky-pull-request-comment@v2.9.0
if: github.ref != 'refs/heads/main'
with:
header: ${{ env.COMMENT_ID }}
message: |
⚡️ Lighthouse report
![loading](https://github.com/bartvdbraak/hellob.art/assets/3996360/0e00b3fc-d5f9-490b-9aa7-07cb4b59f85f)
- name: Set variables based on trigger
run: |
if [[ ${{ github.ref == 'refs/heads/main' }} == true ]]; then
echo "CLOUDFLARE_BRANCH=main" >> $GITHUB_ENV
echo "CLOUDFLARE_URL=https://${{ env.CLOUDFLARE_PROJECT }}.pages.dev" >> $GITHUB_ENV
else
echo "CLOUDFLARE_BRANCH=pull-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "CLOUDFLARE_URL=https://pull-${{ github.event.pull_request.number }}.${{ env.CLOUDFLARE_PROJECT }}.pages.dev" >> $GITHUB_ENV
fi
- name: Checkout repository
uses: actions/checkout@v4.1.1
- name: Setup pnpm
uses: pnpm/action-setup@v3.0.0
with:
version: latest
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Retrieve Vercel Preview URL
uses: zentered/vercel-preview-url@v1.1.9
id: vercel_preview_url
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
with:
vercel_project_id: ${{ vars.VERCEL_PROJECT_ID }}
- name: Await Vercel Deployment
uses: UnlyEd/github-action-await-vercel@v1.2.43
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
with:
deployment-url: ${{ steps.vercel_preview_url.outputs.preview_url }}
timeout: 360
- name: Install Dependencies
run: pnpm install -g @unlighthouse/cli puppeteer
- name: Run Unlighthouse
run: |
unlighthouse-ci \
--site "${{ github.ref == 'refs/heads/main' && 'https://hellob.art' || steps.vercel_preview_url.outputs.preview_url }}" \
--reporter jsonExpanded \
--build-static
- name: Upload report to Cloudflare pages
uses: cloudflare/wrangler-action@v3.4.1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy .unlighthouse --project-name="${{ env.CLOUDFLARE_PROJECT }}" --branch=${{ env.CLOUDFLARE_BRANCH }}
- name: Create result content
id: create_result_content
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const result = JSON.parse(fs.readFileSync('.unlighthouse/ci-result.json', 'utf8'));
const formatScore = score => `${Math.round(score * 100)} (${score})`;
const getEmoji = score => score >= 0.9 ? '🟢' : score >= 0.5 ? '🟠' : '🔴';
const getColor = score => score >= 0.9 ? '4c1' : score >= 0.5 ? 'ffa400' : 'eb0f00';
const score = res => `${getEmoji(res)} ${formatScore(res)}`;
const reportUrl = `${{ env.CLOUDFLARE_URL }}`;
const comment = [
`⚡️ Lighthouse report for the changes in this PR:`,
'| Category | Score |',
'| --- | --- |',
`| Performance | ${score(result.summary.categories.performance.averageScore)} |`,
`| Accessibility | ${score(result.summary.categories.accessibility.averageScore)} |`,
`| Best practices | ${score(result.summary.categories['best-practices'].averageScore)} |`,
`| SEO | ${score(result.summary.categories.seo.averageScore)} |`,
`| *Overall* | ${score(result.summary.score)} |`,
'',
'*Lighthouse scores for individual routes:*',
'',
'| Path | Performance | Accessibility | Best practices | SEO | Overall |',
'| --- | --- | --- | --- | --- | --- |',
`${result.routes.map(route => `| ${route.path} | ${score(route.categories.performance.score)} | ${score(route.categories.accessibility.score)} | ${score(route.categories['best-practices'].score)} | ${score(route.categories.seo.score)} | ${score(route.score)} |`).join('\n')}`,
'',
'*Lighthouse metrics:*',
'',
'| Metric | Average Value |',
'| --- | --- |',
`${Object.entries(result.summary.metrics).map(([metric, { averageNumericValue }]) => `| ${metric} | ${averageNumericValue} |`).join('\n')}`,
'',
`View the full Lighthouse report [here](${reportUrl}).`,
].join('\n');
core.setOutput("comment", comment);
core.setOutput("score", `${Math.round(result.summary.score * 100)}`);
core.setOutput("scoreColor", getColor(result.summary.score));
- name: Update comment with result
uses: marocchino/sticky-pull-request-comment@v2.9.0
if: github.ref != 'refs/heads/main'
with:
header: ${{ env.COMMENT_ID }}
message: ${{ steps.create_result_content.outputs.comment }}
- name: Create Lighthouse Score badge
uses: schneegans/dynamic-badges-action@v1.7.0
if: github.ref == 'refs/heads/main'
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: 795a3d6af5b0db5754cf7279898c3c16
filename: hellob.art-unlighthouse.json
namedLogo: Lighthouse
label: lighthouse
message: ${{ steps.create_result_content.outputs.score }}
color: ${{ steps.create_result_content.outputs.scoreColor }}
- name: Update comment on failure
uses: marocchino/sticky-pull-request-comment@v2.9.0
if: failure() && github.ref != 'refs/heads/main'
with:
header: ${{ env.COMMENT_ID }}
message: |
⚡️ Lighthouse report failed
See deployment for any errors
- name: Update comment on cancel
uses: marocchino/sticky-pull-request-comment@v2.9.0
if: cancelled() && github.ref != 'refs/heads/main'
with:
header: ${{ env.COMMENT_ID }}
message: |
⚡️ Lighthouse report cancelled

48
.gitignore vendored
View file

@ -1,40 +1,10 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
yarn-error.log*
pnpm-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# unlighthouse
.unlighthouse
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -1,15 +1,4 @@
cache
.cache
package.json
package-lock.json
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
public
CHANGELOG.md
.yarn
dist
node_modules
.next
build
.contentlayer
.github
.unlighthouse
package-lock.json
yarn.lock

15
.prettierrc Normal file
View file

@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View file

@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
Omnidash is a self-hostable dashboard using connectors to a multitude of ticketing systems.
Copyright (C) 2024 Bart van der Braak
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
Omnidash Copyright (C) 2024 Bart van der Braak
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View file

@ -1,117 +0,0 @@
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
BarChart,
Database,
FileKey,
Filter,
FormInput,
Home,
} from "lucide-react";
import { ChannelLink } from "./channelLink";
import { TeamSwitcher } from "./TeamSwitcher";
import Link from "next/link";
type Props = {
navigation: {
href: string;
external?: boolean;
label: string;
}[];
channels: {
name: string;
}[];
};
export const DesktopSidebar: React.FC<Props> = ({ navigation, channels }) => {
return (
<aside className="relative hidden min-h-screen pb-12 border-r lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col border-white/10">
<Link
href="/overview"
className="flex items-center gap-2 px-8 py-6 text-2xl font-semibold tracking-tight duration-200 stroke-zinc-800 dark:text-zinc-200 dark:stroke-zinc-500 dark:hover:stroke-white hover:stroke-zinc-700 hover:text-zinc-700 dark:hover:text-white"
>
<Logo className="w-8 h-8 duration-200 " />
Omnidash
</Link>
<div className="space-y-4">
<div className="px-6 py-2">
<h2 className="px-2 mb-2 text-lg font-semibold tracking-tight">
{/* Events */}
</h2>
<div className="space-y-1">
<Link href="/overview">
<Button
variant="ghost"
size="sm"
className="justify-start w-full"
>
<Home className="w-4 h-4 mr-2" />
Overview
</Button>
</Link>
<Link href="/keys">
<Button
variant="ghost"
size="sm"
className="justify-start w-full"
>
<FileKey className="w-4 h-4 mr-2" />
API Keys
</Button>
</Link>
<Link href="/channels">
<Button
variant="ghost"
size="sm"
className="justify-start w-full"
>
<Database className="w-4 h-4 mr-2" />
Channels
</Button>
</Link>
<Button
variant="ghost"
disabled
size="sm"
className="justify-start w-full"
>
<Filter className="w-4 h-4 mr-2" />
Filter
</Button>
<Button
variant="ghost"
disabled
size="sm"
className="justify-start w-full"
>
<BarChart className="w-4 h-4 mr-2" />
Analytics
</Button>
</div>
</div>
<div className="py-2">
<h2 className="relative px-8 text-lg font-semibold tracking-tight">
Channels
</h2>
<ScrollArea className="h-[230px] px-4">
<div className="p-2 space-y-1">
{channels
.sort((a, b) => a.name.localeCompare(b.name))
.map((channel) => (
<ChannelLink
key={channel.name}
href={`/channels/${channel.name}`}
channelName={channel.name}
/>
))}
</div>
</ScrollArea>
</div>
</div>
<div className="absolute inset-x-0 mx-6 bottom-8">
<TeamSwitcher />
</div>
</aside>
);
};

View file

@ -1,130 +0,0 @@
"use client";
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { BarChart, Database, FileKey, Filter, Home, Menu } from "lucide-react";
import { ChannelLink } from "./channelLink";
import { TeamSwitcher } from "./TeamSwitcher";
import Link from "next/link";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
type Props = {
navigation: {
href: string;
external?: boolean;
label: string;
}[];
channels: {
name: string;
}[];
};
export const MobileSidebar: React.FC<Props> = ({ channels }) => {
return (
<div className="lg:hidden">
<Sheet>
<div className="sticky top-0 z-40 flex items-center justify-end w-full px-4 py-4 bg-zinc-950 gap-x-6 sm:px-6 lg:hidden">
<SheetTrigger>
<Menu />
</SheetTrigger>
</div>
<SheetContent position="bottom" size="content">
<SheetHeader>
<SheetTitle className="flex items-center justify-center gap-2">
{" "}
<Logo className="w-8 h-8 stroke-zinc-300" />
Omnidash
</SheetTitle>
{/* <SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription> */}
</SheetHeader>
<div className="space-y-4">
<div className="px-6 py-2">
<h2 className="px-2 mb-2 text-lg font-semibold tracking-tight">
{/* Events */}
</h2>
<div className="space-y-1">
<Link href="/overview">
<Button
variant="ghost"
size="sm"
className="justify-start w-full"
>
<Home className="w-4 h-4 mr-2" />
Overview
</Button>
</Link>
<Link href="/keys">
<Button
variant="ghost"
size="sm"
className="justify-start w-full"
>
<FileKey className="w-4 h-4 mr-2" />
API Keys
</Button>
</Link>
<Link href="/channels">
<Button
variant="ghost"
size="sm"
className="justify-start w-full"
>
<Database className="w-4 h-4 mr-2" />
Channels
</Button>
</Link>
<Button
variant="ghost"
disabled
size="sm"
className="justify-start w-full"
>
<Filter className="w-4 h-4 mr-2" />
Filter
</Button>
<Button
variant="ghost"
disabled
size="sm"
className="justify-start w-full"
>
<BarChart className="w-4 h-4 mr-2" />
Analytics
</Button>
</div>
</div>
<div className="py-2">
<h2 className="relative px-8 text-lg font-semibold tracking-tight">
Events
</h2>
<ScrollArea className="h-[230px] px-4">
<div className="p-2 space-y-1">
{channels.map((channel) => (
<ChannelLink
key={channel.name}
href={`/channels/${channel.name}`}
channelName={channel.name}
/>
))}
</div>
</ScrollArea>
</div>
</div>
<SheetFooter>
<TeamSwitcher />
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
};

View file

@ -1,94 +0,0 @@
"use client";
import {
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { ChevronsUpDown, LogOut } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Loading } from "@/components/loading";
import {
useAuth,
useOrganization,
useOrganizationList,
useUser,
} from "@clerk/clerk-react";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { AvatarFallback } from "@radix-ui/react-avatar";
type Props = {};
export const TeamSwitcher: React.FC<Props> = (): JSX.Element => {
const { setActive, organizationList } = useOrganizationList();
const { organization: currentOrg } = useOrganization();
const { signOut } = useAuth();
const { user } = useUser();
const router = useRouter();
const [loading, setLoading] = useState(false);
async function changeOrg(id: string | null) {
if (!setActive) {
return;
}
try {
setLoading(true);
await setActive({ organization: id });
router.refresh();
} finally {
setLoading(false);
}
}
return (
<DropdownMenu>
{loading ? (
<Loading />
) : (
<DropdownMenuTrigger className="flex items-center justify-between w-full px-2 py-1 rounded gap-4 hover:bg-zinc-100 dark:hover:bg-zinc-700">
<div className="flex items-center justify-start w-full gap-4 ">
<Avatar>
{user?.profileImageUrl ? (
<AvatarImage
src={user.profileImageUrl}
alt={user.username ?? "Profile picture"}
/>
) : null}
<AvatarFallback className="flex items-center justify-center w-8 h-8 overflow-hidden border rounded-md bg-zinc-100 border-zinc-500 text-zinc-700">
{(currentOrg?.slug ?? user?.username ?? "")
.slice(0, 2)
.toUpperCase() ?? "P"}
</AvatarFallback>
</Avatar>
<span>{currentOrg?.name ?? "Personal"}</span>
</div>
{/* <PlanBadge plan={currentTeam?.plan ?? "DISABLED"} /> */}
<ChevronsUpDown className="w-4 h-4" />
</DropdownMenuTrigger>
)}
<DropdownMenuContent className="w-full lg:w-56" align="end" forceMount>
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<button
onClick={async () => {
await signOut();
router.refresh();
}}
className="w-full"
>
<LogOut className="w-4 h-4 mr-2" />
<span>Sign out</span>
</button>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View file

@ -1,28 +0,0 @@
"use client";
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";
import { Hash } from "lucide-react";
import { Button } from "@/components/ui/button";
type Props = {
href: string;
channelName: string | null;
};
export const ChannelLink: React.FC<Props> = ({ href, channelName }) => {
const isActive = channelName === useSelectedLayoutSegments().at(1);
return (
<Link href={href}>
<Button
variant={isActive ? "subtle" : "ghost"}
size="sm"
className="justify-start w-full font-normal"
>
<Hash className="w-4 h-4 mr-2" />
{channelName}
</Button>
</Link>
);
};

View file

@ -1,22 +0,0 @@
import { DesktopSidebar } from "./DesktopSidebar";
import { MobileSidebar } from "./MobileSidebar";
import { getTenantId } from "@/lib/auth";
import { Fragment } from "react";
interface LayoutProps {
children: React.ReactNode;
}
export default async function Layout({ children }: LayoutProps) {
const tenantId = getTenantId();
return (
<Fragment>
<DesktopSidebar channels={[]} navigation={[]} />
<MobileSidebar channels={[]} navigation={[]} />
<div className=" lg:pl-72">{children}</div>
</Fragment>
);
}

View file

@ -1,9 +0,0 @@
import { Loading as Spinner } from "@/components/loading";
export default function Loading() {
return (
<div className="flex items-center justify-center w-full h-screen">
<Spinner className="text-white" />
</div>
);
}

View file

@ -1,111 +0,0 @@
import { cn } from "@/lib/utils";
export default async function Page(_props: { params: { tenantSlug: string } }) {
const stats: {
label: string;
value: string;
}[] = [
{
label: "Total Channels",
value: "0",
},
{
label: "Total Events (7 days)",
value: "0",
},
];
return (
<main>
<div className="relative overflow-hidden isolate">
{/* Stats */}
<div className="border-b border-b-white/10 ">
<div className="flex flex-col items-start justify-between h-16 px-4 py-4 border-b bg-primary-900 gap-x-8 gap-y-4 sm:flex-row sm:items-center sm:px-6 lg:px-8 border-white/10">
<div>
<div className="flex items-center gap-x-3 ">
{/* <div className="flex-none p-1 text-green-400 rounded-full bg-green-400/10">
<div className="w-2 h-2 rounded-full bg-current" />
</div> */}
<h1 className="flex text-base gap-x-2 leading-7">
<span className="font-semibold text-white">
{"Personal Account"}
</span>
</h1>
</div>
{/* <p className="mt-2 text-xs leading-6 text-zinc-400">{channel.description}</p> */}
</div>
<div className="flex-none order-first px-2 py-1 text-xs font-medium rounded-full bg-rose-400/10 text-rose-400 ring-1 ring-inset ring-rose-400/30 sm:order-none">
test
</div>
</div>
<dl
className={cn(
"grid grid-cols-1 bg-zinc-700/10 sm:grid-cols-2 border-b border-white/10 h-32",
{
"lg:grid-cols-2": stats.length === 2,
"lg:grid-cols-3": stats.length === 3,
"lg:grid-cols-4": stats.length >= 4,
}
)}
>
{" "}
{stats.map((stat, statIdx) => (
<div
key={stat.label}
className={cn(
statIdx % 2 === 1
? "sm:border-l"
: statIdx === 2
? "lg:border-l"
: "",
"flex items-baseline flex-wrap justify-between gap-y-2 gap-x-4 border-t border-zinc-100/5 px-4 py-10 sm:px-6 lg:border-t-0 xl:px-8"
)}
>
<dt className="text-sm font-medium leading-6 text-zinc-500">
{stat.label}
</dt>
{/* <dd
className={cn(
stat.changeType === 'negative' ? 'text-rose-600' : 'text-zinc-700',
'text-xs font-medium'
)}
>
{stat.change}
</dd> */}
<dd className="flex-none w-full text-3xl font-medium tracking-tight leading-10 text-zinc-100">
{stat.value}
</dd>
</div>
))}
</dl>
</div>
</div>
<div className="py-16 space-y-16 xl:space-y-20">
{/* Recent activity table */}
<div>
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<h2 className="max-w-2xl mx-auto text-base font-semibold leading-6 text-zinc-100 lg:mx-0 lg:max-w-none">
Recent events
</h2>
</div>
<div className="mt-6 overflow-x-hidden overflow-y-scroll border-t border-zinc-900">
<div className="mx-auto max-w-7xl ">
<div className="max-w-2xl mx-auto lg:mx-0 lg:max-w-none">
<table className="w-full text-left ">
<thead className="sr-only">
<tr>
<th>Event</th>
<th className="hidden sm:table-cell">Content</th>
<th>More details</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
);
}

View file

@ -1,31 +0,0 @@
import { Particles } from "@/components/landing/particles";
import {
ClerkProvider,
SignIn,
SignedIn,
SignedOut,
} from "@clerk/nextjs/app-beta";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<SignedIn>{children}</SignedIn>
<SignedOut>
<div className="flex items-center justify-center w-screen h-screen">
<Particles className="absolute inset-0 -z-10 " />
<SignIn
appearance={{
variables: {
colorPrimary: "#161616",
colorText: "#161616",
},
}}
afterSignInUrl={"/overview"}
afterSignUpUrl={"/overview"}
/>
</div>
</SignedOut>
</ClerkProvider>
);
}

View file

@ -1,82 +0,0 @@
/* Custom AOS distance */
@media screen {
html:not(.no-js) [data-aos="fade-up"] {
-webkit-transform: translate3d(0, 14px, 0);
transform: translate3d(0, 14px, 0);
}
html:not(.no-js) [data-aos="fade-down"] {
-webkit-transform: translate3d(0, -14px, 0);
transform: translate3d(0, -14px, 0);
}
html:not(.no-js) [data-aos="fade-right"] {
-webkit-transform: translate3d(-14px, 0, 0);
transform: translate3d(-14px, 0, 0);
}
html:not(.no-js) [data-aos="fade-left"] {
-webkit-transform: translate3d(14px, 0, 0);
transform: translate3d(14px, 0, 0);
}
html:not(.no-js) [data-aos="fade-up-right"] {
-webkit-transform: translate3d(-14px, 14px, 0);
transform: translate3d(-14px, 14px, 0);
}
html:not(.no-js) [data-aos="fade-up-left"] {
-webkit-transform: translate3d(14px, 14px, 0);
transform: translate3d(14px, 14px, 0);
}
html:not(.no-js) [data-aos="fade-down-right"] {
-webkit-transform: translate3d(-14px, -14px, 0);
transform: translate3d(-14px, -14px, 0);
}
html:not(.no-js) [data-aos="fade-down-left"] {
-webkit-transform: translate3d(14px, -14px, 0);
transform: translate3d(14px, -14px, 0);
}
html:not(.no-js) [data-aos="zoom-in-up"] {
-webkit-transform: translate3d(0, 14px, 0) scale(0.6);
transform: translate3d(0, 14px, 0) scale(0.6);
}
html:not(.no-js) [data-aos="zoom-in-down"] {
-webkit-transform: translate3d(0, -14px, 0) scale(0.6);
transform: translate3d(0, -14px, 0) scale(0.6);
}
html:not(.no-js) [data-aos="zoom-in-right"] {
-webkit-transform: translate3d(-14px, 0, 0) scale(0.6);
transform: translate3d(-14px, 0, 0) scale(0.6);
}
html:not(.no-js) [data-aos="zoom-in-left"] {
-webkit-transform: translate3d(14px, 0, 0) scale(0.6);
transform: translate3d(14px, 0, 0) scale(0.6);
}
html:not(.no-js) [data-aos="zoom-out-up"] {
-webkit-transform: translate3d(0, 14px, 0) scale(1.2);
transform: translate3d(0, 14px, 0) scale(1.2);
}
html:not(.no-js) [data-aos="zoom-out-down"] {
-webkit-transform: translate3d(0, -14px, 0) scale(1.2);
transform: translate3d(0, -14px, 0) scale(1.2);
}
html:not(.no-js) [data-aos="zoom-out-right"] {
-webkit-transform: translate3d(-14px, 0, 0) scale(1.2);
transform: translate3d(-14px, 0, 0) scale(1.2);
}
html:not(.no-js) [data-aos="zoom-out-left"] {
-webkit-transform: translate3d(14px, 0, 0) scale(1.2);
transform: translate3d(14px, 0, 0) scale(1.2);
}
}

View file

@ -1,13 +0,0 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "additional-styles/theme.css";
@import "tailwindcss/utilities";
/* Additional Tailwind directives: https://tailwindcss.com/docs/functions-and-directives/#responsive */
@layer utilities {
.rtl {
direction: rtl;
}
}

View file

@ -1,66 +0,0 @@
"use client";
import { useEffect } from "react";
import AOS from "aos";
import "aos/dist/aos.css";
import "./css/style.css";
import { Header } from "@/components/landing/ui/header";
import Link from "next/link";
export default function DefaultLayout({
children,
}: {
children: React.ReactNode;
}) {
useEffect(() => {
AOS.init({
once: true,
disable: "phone",
duration: 1000,
easing: "ease-out-cubic",
});
});
return (
<>
<Header />
<main className="grow">{children}</main>
<footer className="pt-24 " aria-labelledby="footer-heading">
<h2 id="footer-heading" className="sr-only">
Footer
</h2>
<div className="px-6 pb-8 mx-auto max-w-7xl lg:px-8">
<div className="pt-8 mt-16 sm:mt-20 md:flex md:items-center md:justify-between lg:mt-24">
<div className="flex space-x-6 md:order-2">
<Link
target="_blank"
href="https://github.com/bartvdbraak/omnidash"
className="text-gray-500 hover:text-gray-400"
>
<span className="sr-only">Github</span>
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
</Link>
</div>
<p className="mt-8 text-xs text-gray-400 leading-5 md:order-1 md:mt-0">
&copy; {new Date().getUTCFullYear()} All rights reserved.
</p>
</div>
</div>
</footer>
</>
);
}

View file

@ -1,13 +0,0 @@
import { Hero } from "@/components/landing/hero";
import { Features } from "@/components/landing/features";
import { Cta } from "@/components/landing/cta";
export default function Page() {
return (
<div className="overflow-x-hidden max-w-screen">
<Hero />
<Features />
<Cta />
</div>
);
}

View file

@ -1,79 +0,0 @@
import { Inter } from "next/font/google";
import LocalFont from "next/font/local";
import { ThemeProvider } from "@/components/theme-provider";
import "tailwindcss/tailwind.css";
import { ToastProvider } from "../toast-provider";
import { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "Omnidash",
template: "%s | Omnidash",
},
description: "Open Source Multi-client Ticket Dashboard",
metadataBase: new URL("https://omnidash.io"),
openGraph: {
title: "Omnidash",
description: "Open Source Multi-client Ticket Dashboard",
url: "https://omnidash.io",
siteName: "omnidash.io",
locale: "en-US",
type: "website",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
twitter: {
title: "Omnidash",
card: "summary_large_image",
},
icons: {
shortcut: "/favicon.png",
},
};
interface RootLayoutProps {
children: React.ReactNode;
}
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
const calSans = LocalFont({
src: "../public/fonts/CalSans-SemiBold.ttf",
variable: "--font-calsans",
});
interface RootLayoutProps {
children: React.ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
return (
<>
<html
lang="en"
suppressHydrationWarning
className={[inter.variable, calSans.variable].join(" ")}
>
<head />
<body className="min-h-screen antialiased">
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<ToastProvider>{children}</ToastProvider>
</ThemeProvider>
</body>
</html>
</>
);
}

1
backend/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/pb_data

17
backend/LICENSE.md Normal file
View file

@ -0,0 +1,17 @@
The MIT License (MIT)
Copyright (c) 2022 - present, Gani Georgiev
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,33 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
// add
collection.schema.addField(new SchemaField({
"system": false,
"id": "rncq13xn",
"name": "appearance_mode",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"light",
"dark"
]
}
}))
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
// remove
collection.schema.removeField("rncq13xn")
return dao.saveCollection(collection)
})

View file

@ -0,0 +1,49 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "rncq13xn",
"name": "appearance_mode",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"light",
"dark",
"system"
]
}
}))
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "rncq13xn",
"name": "appearance_mode",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"light",
"dark"
]
}
}))
return dao.saveCollection(collection)
})

View file

@ -0,0 +1,50 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "rncq13xn",
"name": "appearanceMode",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"light",
"dark",
"system"
]
}
}))
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "rncq13xn",
"name": "appearance_mode",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"light",
"dark",
"system"
]
}
}))
return dao.saveCollection(collection)
})

BIN
backend/pocketbase Executable file

Binary file not shown.

13
components.json Normal file
View file

@ -0,0 +1,13 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.pcss",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
}
}

View file

@ -1,117 +0,0 @@
import {
AlertTriangle,
ArrowRight,
Check,
ChevronLeft,
ChevronRight,
Circle,
ClipboardCheck,
Copy,
CreditCard,
File,
FileText,
HelpCircle,
Image,
Laptop,
Loader2,
LucideProps,
Moon,
MoreVertical,
Pizza,
Plus,
Settings,
SunMedium,
Trash,
Twitter,
User,
X,
type Icon as LucideIcon,
} from "lucide-react";
export type Icon = LucideIcon;
export const Icons = {
logo: (props: LucideProps) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<circle cx="12" cy="12" r="10" />
</svg>
),
close: X,
spinner: Loader2,
chevronLeft: ChevronLeft,
chevronRight: ChevronRight,
trash: Trash,
post: FileText,
page: File,
media: Image,
settings: Settings,
billing: CreditCard,
ellipsis: MoreVertical,
add: Plus,
warning: AlertTriangle,
user: User,
arrowRight: ArrowRight,
help: HelpCircle,
pizza: Pizza,
twitter: Twitter,
check: Check,
copy: Copy,
copyDone: ClipboardCheck,
sun: SunMedium,
moon: Moon,
laptop: Laptop,
gitHub: (props: LucideProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
/>
</svg>
),
radix: (props: LucideProps) => (
<svg viewBox="0 0 25 25" fill="none" {...props}>
<path
d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"
fill="currentcolor"
/>
<path d="M12 0H4V8H12V0Z" fill="currentcolor" />
<path
d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"
fill="currentcolor"
/>
</svg>
),
npm: (props: LucideProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
</svg>
),
yarn: (props: LucideProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z" />
</svg>
),
pnpm: (props: LucideProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z" />
</svg>
),
react: (props: LucideProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" />
</svg>
),
tailwind: (props: LucideProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z" />
</svg>
),
};

View file

@ -1,49 +0,0 @@
import { ArrowRight } from "lucide-react";
import Link from "next/link";
export const Cta: React.FC = () => {
return (
<section>
<div className="max-w-6xl px-4 mx-auto sm:px-6">
<div className="relative px-8 py-12 md:py-20 lg:py-32 rounded-[3rem] overflow-hidden">
{/* Radial gradient */}
<div
className="absolute top-0 flex items-center justify-center w-1/3 pointer-events-none -translate-y-1/2 left-1/2 -translate-x-1/2 -z-10 aspect-square"
aria-hidden="true"
>
<div className="absolute inset-0 translate-z-0 bg-primary-500 rounded-full blur-[120px] opacity-70" />
<div className="absolute w-1/4 h-1/4 translate-z-0 bg-primary-400 rounded-full blur-[40px]" />
</div>
{/* Blurred shape */}
<div
className="absolute bottom-0 left-0 opacity-50 pointer-events-none translate-y-1/2 blur-2xl -z-10"
aria-hidden="true"
/>
{/* Content */}
<div className="max-w-3xl mx-auto text-center">
<div>
<div className="inline-flex pb-3 font-medium text-transparent bg-clip-text bg-gradient-to-r from-primary-500 to-primary-200">
The best way to run operations
</div>
</div>
<h2 className="pb-4 text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60">
Simplify your workflows
</h2>
<p className="mb-8 text-lg text-zinc-400">
A consolidated ticket dashboard within 60 seconds.
</p>
<div>
<Link
className=" justify-center flex sm:inline-flex items-center whitespace-nowrap transition duration-150 ease-in-out font-medium rounded px-4 py-1.5 text-zinc-900 bg-gradient-to-r from-white/80 via-white to-white/80 hover:bg-white group"
href="/"
>
Get Started{" "}
<ArrowRight className="w-3 h-3 tracking-normal text-primary-500 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1" />
</Link>
</div>
</div>
</div>
</div>
</section>
);
};

View file

@ -1,110 +0,0 @@
import Image from "next/image";
import GlowTop from "@/public/images/glow-top.svg";
import { Eye, Unplug, Compass, Zap } from "lucide-react";
export const Features: React.FC = () => {
const features = [
{
icon: Unplug,
name: "Effortless Consolidation",
description:
"Consolidate all tickets from multiple platforms and clients effortlessly",
},
{
icon: Eye,
name: "Unparalleled Visibility",
description:
"Gain complete control and visibility over your ticketing operations",
},
{
icon: Compass,
name: "Intuitive Navigation",
description:
"Seamlessly navigate and find tickets with smart filters and advanced search",
},
{
icon: Zap,
name: "Enhanced Efficiency",
description:
"Maximize productivity and resource allocation in ticket management",
},
];
return (
<section>
<div className="relative max-w-6xl px-4 mx-auto sm:px-6">
<div
className="absolute inset-0 -z-10 -mx-28 rounded-t-[3rem] pointer-events-none overflow-hidden"
aria-hidden="true"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 -z-10">
<Image
src={GlowTop}
className="max-w-none"
width={1404}
height={658}
alt="Features Illustration"
/>
</div>
</div>
<div className="pt-16 pb-12 md:pt-52 md:pb-20">
<div>
{/* Section content */}
<div className="flex flex-col max-w-xl mx-auto md:max-w-none md:flex-row md:space-x-8 lg:space-x-16 xl:space-x-20 space-y-8 space-y-reverse md:space-y-0">
{/* Content */}
<div
className="order-1 md:w-7/12 lg:w-1/2 md:order-none max-md:text-center"
data-aos="fade-down"
>
{/* Content #1 */}
<div>
<div className="inline-flex pb-3 font-medium text-transparent bg-clip-text bg-gradient-to-r from-primary-500 to-primary-200">
Centralized view of all tickets
</div>
</div>
<h3 className="pb-3 text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60">
Reduce Context Switching
</h3>
<p className="mb-8 text-lg text-zinc-400">
Empower your operations teams with by consolidating all ticket
information in one place. Seamlessly filter, sort, and
customize ticket views to meet their unique needs.
</p>
<dl className="max-w-xl grid grid-cols-1 gap-4 lg:max-w-none">
{features.map((feature) => (
<div
key={feature.name}
className="px-2 py-1 rounded group hover:bg-zinc-100 duration-500"
>
<div className="flex items-center mb-1 space-x-2 ">
<feature.icon className="w-4 h-4 shrink-0 text-zinc-300 group-hover:text-zinc-950 duration-500" />
<h4 className="font-medium text-zinc-50 group-hover:text-zinc-950 duration-500">
{feature.name}
</h4>
</div>
<p className="text-sm text-left text-zinc-400 group-hover:text-zinc-950 duration-500">
{feature.description}
</p>
</div>
))}
</dl>
</div>
<div className="flex max-w-2xl mx-auto mt-16 md:w-5/12 lg:w-1/2 sm:mt-24 lg:ml-10 lg:mr-0 lg:mt-0 lg:max-w-none lg:flex-none xl:ml-32">
<div className="z-10 flex-none max-w-3xl sm:max-w-5xl lg:max-w-none">
<Image
src="/screenshots/demo.png"
alt="App screenshot"
width={2432}
height={1442}
className="w-[76rem] z-10 rounded-xl border border-white/10"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};

View file

@ -1,71 +0,0 @@
import { Particles } from "./particles";
import ReactWrapBalancer from "react-wrap-balancer";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
export const Hero: React.FC = () => {
return (
<section>
<div className="relative max-w-6xl min-h-screen px-4 mx-auto sm:px-6">
{/* Particles animation */}
<Particles className="absolute inset-0 -z-10 " />
<div className="pt-32 pb-16 md:pt-52 md:pb-32">
{/* Hero content */}
<div className="container mx-auto text-center">
<div className="mb-6" data-aos="fade-down">
<div className="relative inline-flex before:absolute before:inset-0 ">
<Link
className="px-3 py-1 text-sm font-medium inline-flex items-center justify-center border border-transparent rounded-full text-zinc-300 hover:text-white transition duration-150 ease-in-out w-full group [background:linear-gradient(theme(colors.primary.900),_theme(colors.primary.900))_padding-box,_conic-gradient(theme(colors.primary.400),_theme(colors.primary.700)_25%,_theme(colors.primary.700)_75%,_theme(colors.primary.400)_100%)_border-box] relative before:absolute before:inset-0 before:bg-zinc-800/30 before:rounded-full before:pointer-events-none"
href="https://github.com/bartvdbraak/omnidash"
>
<span className="relative inline-flex items-center">
Omnidash is Open Source{" "}
<span className="tracking-normal text-primary-500 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">
-&gt;
</span>
</span>
</Link>
</div>
</div>
<h1
className="pb-4 font-extrabold tracking-tight text-transparent text-6xl lg:text-8xl bg-clip-text bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60"
data-aos="fade-down"
>
<ReactWrapBalancer>
One Dashboard, Countless Solutions
</ReactWrapBalancer>
</h1>
<p
className="mb-8 text-lg text-zinc-300"
data-aos="fade-down"
data-aos-delay="200"
>
Tame ticket overload and keep your operation teams sane
</p>
<div
className="flex flex-col items-center max-w-xs mx-auto gap-4 sm:max-w-none sm:justify-center sm:flex-row sm:inline-flex"
data-aos="fade-down"
data-aos-delay="400"
>
<Link
className="w-full justify-center flex items-center whitespace-nowrap transition duration-150 ease-in-out font-medium rounded px-4 py-1.5 text-zinc-900 bg-gradient-to-r from-white/80 via-white to-white/80 hover:bg-white group"
href="/"
>
Get Started{" "}
<ArrowRight className="w-3 h-3 tracking-normal text-primary-500 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1" />
</Link>
<Link
className="w-full transition duration-150 ease-in-out bg-opacity-25 text-zinc-200 hover:text-white bg-zinc-900 hover:bg-opacity-30"
href="https://github.com/bartvdbraak/omnidash"
>
Star on GitHub
</Link>
</div>
</div>
</div>
</div>
</section>
);
};

View file

@ -1,258 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import React, { useRef, useEffect } from "react";
import MousePosition from "./utils/mouse-position";
interface ParticlesProps {
className?: string;
quantity?: number;
staticity?: number;
ease?: number;
refresh?: boolean;
color?: string;
vx?: number;
vy?: number;
}
function hexToRgb(hex: string): number[] {
// Remove the "#" character from the beginning of the hex color code
hex = hex.replace("#", "");
// Convert the hex color code to an integer
const hexInt = parseInt(hex, 16);
// Extract the red, green, and blue components from the hex color code
const red = (hexInt >> 16) & 255;
const green = (hexInt >> 8) & 255;
const blue = hexInt & 255;
// Return an array of the RGB values
return [red, green, blue];
}
export const Particles: React.FC<ParticlesProps> = ({
className = "",
quantity = 30,
staticity = 50,
ease = 50,
refresh = false,
color = "#ffffff",
vx = 0,
vy = 0,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvasContainerRef = useRef<HTMLDivElement>(null);
const context = useRef<CanvasRenderingContext2D | null>(null);
const circles = useRef<any[]>([]);
const mousePosition = MousePosition();
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
useEffect(() => {
if (canvasRef.current) {
context.current = canvasRef.current.getContext("2d");
}
initCanvas();
animate();
window.addEventListener("resize", initCanvas);
return () => {
window.removeEventListener("resize", initCanvas);
};
}, []);
useEffect(() => {
onMouseMove();
}, [mousePosition.x, mousePosition.y]);
useEffect(() => {
initCanvas();
}, [refresh]);
const initCanvas = () => {
resizeCanvas();
drawParticles();
};
const onMouseMove = () => {
if (canvasRef.current) {
const rect = canvasRef.current.getBoundingClientRect();
const { w, h } = canvasSize.current;
const x = mousePosition.x - rect.left - w / 2;
const y = mousePosition.y - rect.top - h / 2;
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
if (inside) {
mouse.current.x = x;
mouse.current.y = y;
}
}
};
type Circle = {
x: number;
y: number;
translateX: number;
translateY: number;
size: number;
alpha: number;
targetAlpha: number;
dx: number;
dy: number;
magnetism: number;
};
const resizeCanvas = () => {
if (canvasContainerRef.current && canvasRef.current && context.current) {
circles.current.length = 0;
canvasSize.current.w = canvasContainerRef.current.offsetWidth;
canvasSize.current.h = canvasContainerRef.current.offsetHeight;
canvasRef.current.width = canvasSize.current.w * dpr;
canvasRef.current.height = canvasSize.current.h * dpr;
canvasRef.current.style.width = `${canvasSize.current.w}px`;
canvasRef.current.style.height = `${canvasSize.current.h}px`;
context.current.scale(dpr, dpr);
}
};
const circleParams = (): Circle => {
const x = Math.floor(Math.random() * canvasSize.current.w);
const y = Math.floor(Math.random() * canvasSize.current.h);
const translateX = 0;
const translateY = 0;
const size = Math.floor(Math.random() * 2) + 1;
const alpha = 0;
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
const dx = (Math.random() - 0.5) * 0.2;
const dy = (Math.random() - 0.5) * 0.2;
const magnetism = 0.1 + Math.random() * 4;
return {
x,
y,
translateX,
translateY,
size,
alpha,
targetAlpha,
dx,
dy,
magnetism,
};
};
const rgb = hexToRgb(color);
const drawCircle = (circle: Circle, update = false) => {
if (context.current) {
const { x, y, translateX, translateY, size, alpha } = circle;
context.current.translate(translateX, translateY);
context.current.beginPath();
context.current.arc(x, y, size, 0, 2 * Math.PI);
context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
context.current.fill();
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!update) {
circles.current.push(circle);
}
}
};
const clearContext = () => {
if (context.current) {
context.current.clearRect(
0,
0,
canvasSize.current.w,
canvasSize.current.h
);
}
};
const drawParticles = () => {
clearContext();
const particleCount = quantity;
for (let i = 0; i < particleCount; i++) {
const circle = circleParams();
drawCircle(circle);
}
};
const remapValue = (
value: number,
start1: number,
end1: number,
start2: number,
end2: number
): number => {
const remapped =
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
};
const animate = () => {
clearContext();
circles.current.forEach((circle: Circle, i: number) => {
// Handle the alpha value
const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = parseFloat(
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2)
);
if (remapClosestEdge > 1) {
circle.alpha += 0.02;
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha;
}
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
}
circle.x += circle.dx + vx;
circle.y += circle.dy + vy;
circle.translateX +=
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
ease;
circle.translateY +=
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
ease;
// circle gets out of the canvas
if (
circle.x < -circle.size ||
circle.x > canvasSize.current.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.current.h + circle.size
) {
// remove the circle from the array
circles.current.splice(i, 1);
// create a new circle
const newCircle = circleParams();
drawCircle(newCircle);
// update the circle position
} else {
drawCircle(
{
...circle,
x: circle.x,
y: circle.y,
translateX: circle.translateX,
translateY: circle.translateY,
alpha: circle.alpha,
},
true
);
}
});
window.requestAnimationFrame(animate);
};
return (
<div className={className} ref={canvasContainerRef} aria-hidden="true">
<canvas ref={canvasRef} />
</div>
);
};

View file

@ -1,31 +0,0 @@
import Link from "next/link";
import { Logo } from "@/components/logo";
export const Header: React.FC = () => {
return (
<header className="absolute z-30 w-full">
<div className="max-w-6xl px-4 mx-auto sm:px-6">
<div className="flex items-center justify-between h-16 md:h-20">
<Link href="/" className="mr-4 shrink-0">
<Logo className="w-8 h-8 stroke-zinc-300 hover:stroke-white duration-500" />
</Link>
{/* Desktop navigation */}
<nav className="flex grow">
{/* Desktop sign in links */}
<ul className="flex flex-wrap items-center justify-end grow">
<li>
<Link
className="text-sm font-medium text-zinc-300 hover:text-white duration-500"
href="/overview"
>
Sign in
</Link>
</li>
</ul>
</nav>
</div>
</div>
</header>
);
};

View file

@ -1,27 +0,0 @@
import { useState, useEffect } from "react";
interface MousePosition {
x: number;
y: number;
}
export default function useMousePosition(): MousePosition {
const [mousePosition, setMousePosition] = useState<MousePosition>({
x: 0,
y: 0,
});
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
setMousePosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return mousePosition;
}

View file

@ -1,44 +0,0 @@
import React, { SVGProps } from "react";
export function Loading({
width = 24,
height = 24,
dur = "0.75s",
}: SVGProps<SVGElement>): JSX.Element {
return (
<svg
className="fill-current"
width={width}
height={height}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="4" cy="12" r="3">
<animate
id="a"
begin="0;b.end-0.25s"
attributeName="r"
dur={dur}
values="3;.2;3"
/>
</circle>
<circle cx="12" cy="12" r="3">
<animate
begin="a.end-0.6s"
attributeName="r"
dur={dur}
values="3;.2;3"
/>
</circle>
<circle cx="20" cy="12" r="3">
<animate
id="b"
begin="a.end-0.45s"
attributeName="r"
dur={dur}
values="3;.2;3"
/>
</circle>
</svg>
);
}

View file

@ -1,24 +0,0 @@
type Props = {
className?: string;
};
export const Logo: React.FC<Props> = ({ className }) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="current"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="7" height="9" x="3" y="3" rx="1" />
<rect width="7" height="5" x="14" y="3" rx="1" />
<rect width="7" height="9" x="14" y="12" rx="1" />
<rect width="7" height="5" x="3" y="16" rx="1" />
</svg>
);
};

View file

@ -1,79 +0,0 @@
"use client";
import * as React from "react";
import Link from "next/link";
// import { docsConfig } from "@/config/docs"
import { cn } from "@/lib/utils";
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
export function MobileNav() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="-ml-4 text-base hover:bg-transparent focus:ring-0 focus:ring-offset-0 md:hidden"
>
<Icons.logo className="w-4 h-4 mr-2" />{" "}
<span className="font-bold">Menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={24}
alignOffset={4}
className="w-[300px] overflow-scroll"
>
<DropdownMenuItem asChild>
<Link href="/" className="flex items-center">
<Icons.logo className="w-4 h-4 mr-2" /> Omnidash
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<ScrollArea className="h-[400px]">
{/* {docsConfig.sidebarNav?.map(
(item, index) =>
item.href && (
<DropdownMenuItem key={index} asChild>
<Link href={item.href}>{item.title}</Link>
</DropdownMenuItem>
)
)}
{docsConfig.sidebarNav.map((item, index) => (
<DropdownMenuGroup key={index}>
<DropdownMenuSeparator
className={cn({
hidden: index === 0,
})}
/>
<DropdownMenuLabel>{item.title}</DropdownMenuLabel>
<DropdownMenuSeparator className="-mx-2" />
{item?.items?.length &&
item.items.map((item) => (
<DropdownMenuItem key={item.title} asChild>
{item.href ? (
<Link href={item.href}>{item.title}</Link>
) : (
item.title
)}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
))} */}
</ScrollArea>
</DropdownMenuContent>
</DropdownMenu>
);
}

View file

@ -1,27 +0,0 @@
type Props = {
title: string;
description?: string;
actions?: React.ReactNode[];
};
export const PageHeader: React.FC<Props> = ({
title,
description,
actions,
}) => {
return (
<div className="flex items-center justify-between">
<div className="space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
{description}
</p>
</div>
<ul className="flex items-center justify-between gap-4">
{(actions ?? []).map((action, i) => (
<li key={i}>{action}</li>
))}
</ul>
</div>
);
};

View file

@ -1,9 +0,0 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View file

@ -1,50 +0,0 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-8 w-8 shrink-0 overflow-hidden rounded-md ",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-700",
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View file

@ -1,54 +0,0 @@
import * as React from "react";
import { VariantProps, cva } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none dark:hover:bg-zinc-800 dark:hover:text-zinc-100 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-zinc-100 dark:data-[state=open]:bg-zinc-800",
{
variants: {
variant: {
default:
"bg-zinc-900 text-white hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900",
destructive:
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
outline:
"bg-transparent border border-zinc-200 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-100",
subtle:
"bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100",
ghost:
"bg-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800 dark:text-zinc-100 dark:hover:text-zinc-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
link: "bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-zinc-900 dark:text-zinc-100 hover:bg-transparent dark:hover:bg-transparent",
},
size: {
square: "h-10 w-10",
default: "h-10 py-2 px-4",
sm: "h-9 px-2 rounded-md",
lg: "h-11 px-8 rounded-md",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View file

@ -1,200 +0,0 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-zinc-100 data-[state=open]:bg-zinc-100 dark:focus:bg-zinc-700 dark:data-[state=open]:bg-zinc-800",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="w-4 h-4 ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"animate-in slide-in-from-left-1 z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-100 bg-white p-1 text-zinc-700 shadow-md dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"animate-in data-[side=right]:slide-in-from-left-2 data-[side=left]:slide-in-from-right-2 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-100 bg-white p-1 text-zinc-700 shadow-md dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-zinc-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-700",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-zinc-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-700",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="w-4 h-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-zinc-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-700",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="w-2 h-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-zinc-900 dark:text-zinc-300",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-700", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-zinc-500", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -1,23 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
return (
<input
className={cn(
"flex h-10 w-full rounded-md border border-zinc-300 bg-transparent py-2 px-3 text-sm placeholder:text-zinc-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-700 dark:text-zinc-50 focus:ring-zinc-200 dark:focus:ring-zinc-50/50",
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = "Input";
export { Input };

View file

@ -1,23 +0,0 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View file

@ -1,48 +0,0 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-zinc-300 dark:bg-zinc-700" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View file

@ -1,234 +0,0 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const portalVariants = cva("fixed inset-0 z-50 flex", {
variants: {
position: {
top: "items-start",
bottom: "items-end",
left: "justify-start",
right: "justify-end",
},
},
defaultVariants: { position: "right" },
});
interface SheetPortalProps
extends SheetPrimitive.DialogPortalProps,
VariantProps<typeof portalVariants> {}
const SheetPortal = ({
position,
className,
children,
...props
}: SheetPortalProps) => (
<SheetPrimitive.Portal className={cn(className)} {...props}>
<div className={portalVariants({ position })}>{children}</div>
</SheetPrimitive.Portal>
);
SheetPortal.displayName = SheetPrimitive.Portal.displayName;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-all duration-100",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-white p-6 opacity-100 dark:bg-zinc-900",
{
variants: {
position: {
top: "animate-in slide-in-from-top w-full duration-300",
bottom: "animate-in slide-in-from-bottom w-full duration-300",
left: "animate-in slide-in-from-left h-full duration-300",
right: "animate-in slide-in-from-right h-full duration-300",
},
size: {
content: "",
default: "",
sm: "",
lg: "",
xl: "",
full: "",
},
},
compoundVariants: [
{
position: ["top", "bottom"],
size: "content",
class: "max-h-screen",
},
{
position: ["top", "bottom"],
size: "default",
class: "h-1/3",
},
{
position: ["top", "bottom"],
size: "sm",
class: "h-1/4",
},
{
position: ["top", "bottom"],
size: "lg",
class: "h-1/2",
},
{
position: ["top", "bottom"],
size: "xl",
class: "h-5/6",
},
{
position: ["top", "bottom"],
size: "full",
class: "h-screen",
},
{
position: ["right", "left"],
size: "content",
class: "max-w-screen",
},
{
position: ["right", "left"],
size: "default",
class: "w-1/3",
},
{
position: ["right", "left"],
size: "sm",
class: "w-1/4",
},
{
position: ["right", "left"],
size: "lg",
class: "w-1/2",
},
{
position: ["right", "left"],
size: "xl",
class: "w-5/6",
},
{
position: ["right", "left"],
size: "full",
class: "w-screen",
},
],
defaultVariants: {
position: "right",
size: "default",
},
}
);
export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
DialogContentProps
>(({ position, size, className, children, ...props }, ref) => (
<SheetPortal position={position}>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ position, size }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-zinc-100 dark:focus:ring-zinc-400 dark:focus:ring-offset-zinc-900 dark:data-[state=open]:bg-zinc-800">
<X className="w-4 h-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-zinc-900",
"dark:text-zinc-50",
className
)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-zinc-500", "dark:text-zinc-400", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View file

@ -1,128 +0,0 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:bottom-0 sm:right-0 sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-zinc-700 last:mt-0 sm:last:mt-4",
{
variants: {
variant: {
default:
"bg-white border-zinc-200 dark:bg-zinc-800 dark:border-zinc-700",
destructive:
"group destructive bg-red-600 text-white border-red-600 dark:border-red-600",
},
},
defaultVariants: {
variant: "default",
},
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-red-100 group-[.destructive]:hover:border-zinc-50 group-[.destructive]:hover:bg-red-100 group-[.destructive]:hover:text-red-600 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:border-zinc-700 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:hover:text-zinc-100 dark:focus:ring-zinc-400 dark:focus:ring-offset-zinc-900 dark:data-[state=open]:bg-zinc-800",
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute top-2 right-2 rounded-md p-1 text-zinc-500 opacity-0 transition-opacity hover:text-zinc-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:hover:text-zinc-50",
className
)}
toast-close=""
{...props}
>
<X className="w-4 h-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View file

@ -1,36 +0,0 @@
"use client";
import { useToast } from "@/hooks/use-toast";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View file

@ -1,190 +0,0 @@
// Inspired by react-hot-toast library
import * as React from "react";
import { ToastActionElement, type ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
};
case "DISMISS_TOAST":
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
};
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) {
dismiss();
}
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View file

@ -1,12 +0,0 @@
import { auth } from "@clerk/nextjs/app-beta";
import { notFound } from "next/navigation";
/**
* Return the tenant id or a 404 not found page.
*
* The auth check should already be done at a higher level, and we're just returning 404 to make typescript happy.
*/
export function getTenantId(): string {
const { userId, orgId } = auth();
return orgId ?? userId ?? notFound();
}

View file

@ -1,6 +0,0 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -1,12 +0,0 @@
import { withClerkMiddleware } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export default withClerkMiddleware((_req: NextRequest) => {
return NextResponse.next();
});
// Stop Middleware running on static files and public folder
export const config = {
matcher: ["/((?!_next|_static|_vercel|[\\w-]+\\.\\w+).*)"],
};

View file

@ -1,9 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
appDir: true,
},
};
module.exports = nextConfig;

View file

@ -1,56 +1,54 @@
{
"name": "omnidash",
"version": "0.1.0",
"private": true,
"author": {
"name": "bartvdbraak",
"url": "https://github.com/bartvdbraak"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"preview": "next build && next start",
"typecheck": "tsc --noEmit",
"format:write": "prettier . --write --cache",
"format:check": "prettier . --check --cache",
"deps:check": "depcheck"
},
"dependencies": {
"@clerk/clerk-react": "^4.22.1",
"@clerk/nextjs": "^4.23.1",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-toast": "^1.1.4",
"@tailwindcss/forms": "^0.5.4",
"aos": "^2.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"eslint": "8.44.0",
"lucide-react": "^0.259.0",
"eslint-config-next": "13.4.10",
"next": "13.4.12",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-wrap-balancer": "^1.0.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.6"
},
"devDependencies": {
"@types/aos": "^3.0.4",
"@types/react": "^18.2.17",
"@types/node": "^20.4.2",
"@types/react-dom": "^18.2.7",
"autoprefixer": "^10.4.14",
"depcheck": "^1.4.3",
"postcss": "^8.4.26",
"prettier": "^2.8.8",
"tailwindcss": "^3.3.3",
"typescript": "^5.1.6"
}
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "8.56.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.9",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"sveltekit-superforms": "^1.13.4",
"tailwindcss": "^3.3.6",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"zod": "^3.22.4"
},
"type": "module",
"dependencies": {
"bits-ui": "^0.15.1",
"clsx": "^2.1.0",
"cmdk-sv": "^0.0.13",
"formsnap": "^0.4.3",
"lucide-svelte": "^0.316.0",
"mode-watcher": "^0.1.2",
"pocketbase": "^0.21.0",
"radix-icons-svelte": "^1.2.1",
"svelte-headless-table": "^0.18.1",
"svelte-sonner": "^0.3.17",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.1.20"
}
}

File diff suppressed because it is too large Load diff

13
postcss.config.cjs Normal file
View file

@ -0,0 +1,13 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="#f5f6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="7" height="9" x="3" y="3" rx="1" />
<rect width="7" height="5" x="14" y="3" rx="1" />
<rect width="7" height="9" x="14" y="12" rx="1" />
<rect width="7" height="5" x="3" y="16" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

View file

@ -1,28 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1404"
height="658">
<defs>
<path id="a" d="M0 0h2324v658H0z" />
<path id="b" d="M0 0h2324v658H0z" />
<radialGradient id="c" cx="50%" cy="0%" r="106.751%" fx="50%" fy="0%"
gradientTransform="matrix(0 1 -.28313 0 .5 -.5)">
<stop offset="0%" stop-color="#a1a1aa" stop-opacity="0.8" />
<stop offset="22.35%" stop-color="#71717a" stop-opacity=".64" />
<stop offset="100%" stop-color="#0F172A" stop-opacity="0" />
</radialGradient>
<linearGradient id="d" x1="19.609%" x2="50%" y1="14.544%" y2="100%">
<stop offset="0%" stop-color="#FFF" />
<stop offset="100%" stop-color="#FFF" stop-opacity="0" />
</linearGradient>
<filter id="e" width="165.1%" height="170.3%" x="-32.5%" y="-35.1%"
filterUnits="objectBoundingBox">
<feGaussianBlur in="SourceGraphic" stdDeviation="50" />
</filter>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(-460)">
<mask id="f" fill="#fff">
<use xlink:href="#b" />
</mask>
<use xlink:href="#b" fill="url(#c)" />
<path fill="url(#d)" d="m629-216 461 369-284 58z" filter="url(#e)" mask="url(#f)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

17
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,17 @@
import PocketBase from 'pocketbase';
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
interface Locals {
pocketBase: PocketBase;
id: string;
email: string;
}
// interface Error {}
// interface PageData {}
// interface Platform {}
}
}
export {};

24
src/app.html Normal file
View file

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
<link rel="mask-icon" href="%sveltekit.assets%/safari-pinned-tab.svg" color="#222222" />
<meta name="msapplication-TileColor" content="#222222" />
<meta name="theme-color" content="#222222" />
%sveltekit.head%
</head>
<body
data-sveltekit-preload-data="hover"
class="min-h-screen bg-background font-sans antialiased"
>
<div style="display: contents" class="relative flex min-h-screen flex-col">
%sveltekit.body%
</div>
</body>
</html>

58
src/app.pcss Normal file
View file

@ -0,0 +1,58 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 72.2% 50.6%;
--primary-foreground: 0 85.7% 97.3%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 72.2% 50.6%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 72.2% 50.6%;
--primary-foreground: 0 85.7% 97.3%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 72.2% 50.6%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

36
src/hooks.server.ts Normal file
View file

@ -0,0 +1,36 @@
import { type Handle } from '@sveltejs/kit';
import PocketBase from 'pocketbase';
import { building, dev } from '$app/environment';
import { SERVER_PB } from '$env/static/private';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.id = '';
event.locals.email = '';
event.locals.pocketBase = new PocketBase(SERVER_PB);
const isAuth: boolean = event.url.pathname === '/auth';
if (isAuth || building) {
event.cookies.set('pb_auth', '', { path: '/' });
return await resolve(event);
}
const pb_auth = event.request.headers.get('cookie') ?? '';
event.locals.pocketBase.authStore.loadFromCookie(pb_auth);
try {
const auth = await event.locals.pocketBase
.collection('users')
.authRefresh<{ id: string; email: string }>();
event.locals.id = auth.record.id;
event.locals.email = auth.record.email;
} catch (_) {
event.locals.pocketBase.authStore.clear();
}
const response = await resolve(event);
const cookie = event.locals.pocketBase.authStore.exportToCookie({
secure: !dev,
sameSite: 'lax'
});
response.headers.append('set-cookie', cookie);
return response;
};

View file

@ -0,0 +1,18 @@
<script lang="ts">
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
export let data: object;
</script>
<Popover.Root>
<Popover.Trigger asChild let:builder>
<Button builders={[builder]} variant="ghost" size="icon" class="block">
{'{}'}
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto">
<SuperDebug label="$layout data" status={false} {data} />
</Popover.Content>
</Popover.Root>

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 384 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"
></path></svg
>

After

Width:  |  Height:  |  Size: 638 B

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M22.2 32A16 16 0 0 0 6 47.8a26.35 26.35 0 0 0 .2 2.8l67.9 412.1a21.77 21.77 0 0 0 21.3 18.2h325.7a16 16 0 0 0 16-13.4L505 50.7a16 16 0 0 0-13.2-18.3 24.58 24.58 0 0 0-2.8-.2L22.2 32zm285.9 297.8h-104l-28.1-147h157.3l-25.2 147z"
></path></svg
>

After

Width:  |  Height:  |  Size: 429 B

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 640 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"
></path></svg
>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 448 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h137.25V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.27c-30.81 0-40.42 19.12-40.42 38.73V256h68.78l-11 71.69h-57.78V480H400a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48z"
></path></svg
>

After

Width:  |  Height:  |  Size: 461 B

View file

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" {...$$restProps}>
<path d="M0 0h24v24H0z" />
<path d="M3 19h18l-9 -15z" />
</svg>

After

Width:  |  Height:  |  Size: 109 B

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M503.5 204.6L502.8 202.8L433.1 21.02C431.7 17.45 429.2 14.43 425.9 12.38C423.5 10.83 420.8 9.865 417.9 9.57C415 9.275 412.2 9.653 409.5 10.68C406.8 11.7 404.4 13.34 402.4 15.46C400.5 17.58 399.1 20.13 398.3 22.9L351.3 166.9H160.8L113.7 22.9C112.9 20.13 111.5 17.59 109.6 15.47C107.6 13.35 105.2 11.72 102.5 10.7C99.86 9.675 96.98 9.295 94.12 9.587C91.26 9.878 88.51 10.83 86.08 12.38C82.84 14.43 80.33 17.45 78.92 21.02L9.267 202.8L8.543 204.6C-1.484 230.8-2.72 259.6 5.023 286.6C12.77 313.5 29.07 337.3 51.47 354.2L51.74 354.4L52.33 354.8L158.3 434.3L210.9 474L242.9 498.2C246.6 500.1 251.2 502.5 255.9 502.5C260.6 502.5 265.2 500.1 268.9 498.2L300.9 474L353.5 434.3L460.2 354.4L460.5 354.1C482.9 337.2 499.2 313.5 506.1 286.6C514.7 259.6 513.5 230.8 503.5 204.6z"
></path></svg
>

After

Width:  |  Height:  |  Size: 967 B

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 488 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
></path></svg
>

After

Width:  |  Height:  |  Size: 450 B

View file

@ -0,0 +1,35 @@
import type { Icon as LucideIcon } from 'lucide-svelte';
import { ArrowRight, Loader2 } from 'lucide-svelte';
import { GithubLogo, VercelLogo, LinkedinLogo } from 'radix-icons-svelte';
import Logo from './logo.svelte';
import Svelte from './svelte.svelte';
import MicrosoftLogo from './microsoft.svelte';
import AppleLogo from './apple.svelte';
import GitLabLogo from './gitlab.svelte';
import BitBucketLogo from './bitbucket.svelte';
import DiscordLogo from './discord.svelte';
import FacebookLogo from './facebook.svelte';
import GoogleLogo from './google.svelte';
import InstagramLogo from './instagram.svelte';
import TwitterLogo from './twitter.svelte';
export type Icon = LucideIcon;
export const Icons = {
logo: Logo,
gitHub: GithubLogo,
microsoft: MicrosoftLogo,
svelte: Svelte,
vercel: VercelLogo,
linkedIn: LinkedinLogo,
spinner: Loader2,
arrowRight: ArrowRight,
apple: AppleLogo,
bitBucket: BitBucketLogo,
gitLab: GitLabLogo,
discord: DiscordLogo,
facebook: FacebookLogo,
google: GoogleLogo,
instagram: InstagramLogo,
twitter: TwitterLogo
};

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 448 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"
></path></svg
>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,24 @@
<script>
export let colours = {
topLeft: '#FF6C22',
topRight: '#FF9209',
bottomLeft: '#FFD099',
bottomRight: '#3B48D3'
};
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="7" height="9" x="3" y="3" rx="1" stroke={colours.topLeft} />
<rect width="7" height="5" x="14" y="3" rx="1" stroke={colours.topRight} />
<rect width="7" height="5" x="3" y="16" rx="1" stroke={colours.bottomLeft} />
<rect width="7" height="9" x="14" y="12" rx="1" stroke={colours.bottomRight} />
</svg>

View file

@ -0,0 +1,12 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
version="1.2"
baseProfile="tiny"
viewBox="0 0 24 24"
{...$$restProps}
><path
d="M10 12.5c0-.3-.2-.5-.5-.5h-6c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.6l6 .7c.3 0 .5-.2.5-.4v-5.9zM11.5 12c-.3 0-.5.2-.5.5v5.9c0 .3.2.5.5.6l9 1c.3 0 .5-.2.5-.4v-7c0-.3-.2-.5-.5-.5l-9-.1zM10 4.7c0-.3-.2-.5-.5-.4l-6 .7c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.5h6c.3 0 .5-.2.5-.5v-5.8zM11.5 4.1c-.3 0-.5.3-.5.6v5.9c0 .3.2.5.5.5h9c.3 0 .5-.2.5-.5v-7c0-.3-.2-.5-.5-.4l-9 .9z"
></path></svg
>

After

Width:  |  Height:  |  Size: 519 B

View file

@ -0,0 +1,15 @@
<svg
class="inline-svg"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
role="img"
viewBox="0 0 24 24"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z"
/></svg
>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
><path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
></path></svg
>

After

Width:  |  Height:  |  Size: 994 B

View file

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" {...$$restProps}>
<path stroke="none" d="M0 0h24v24H0z" />
<path d="M3 19h18l-9 -15z" />
</svg>

After

Width:  |  Height:  |  Size: 123 B

View file

@ -0,0 +1,9 @@
export { default as Metadata } from './metadata.svelte';
export { default as SiteFooter } from './site-footer.svelte';
export { default as SiteNavBar } from './site-navbar.svelte';
export { default as TailwindIndicator } from './tailwind-indicator.svelte';
export { default as ModeToggle } from './mode-toggle.svelte';
export { default as Particles } from './particles.svelte';
export * from './icons';
export * from './nav';

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { page } from '$app/stores';
import { siteConfig } from '$lib/config/site';
export let title: string = siteConfig.name;
$: title = $page.data?.name ? `${$page.data.name} — ${siteConfig.name}` : siteConfig.name;
$: description = $page.data?.subTitle ?? siteConfig.description;
$: ogImage = encodeURI(
`${siteConfig.ogImage}?title=${$page.data.title}&subTitle=${$page.data.subTitle}`
);
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={siteConfig.keywords} />
<meta name="author" content="Bart van der Braak" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteConfig.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
<meta name="twitter:image:alt" content={siteConfig.name} />
<meta name="twitter:creator" content="Bart van der Braak" />
<meta property="og:title" content={title} />
<meta property="og:type" content="article" />
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:alt" content={siteConfig.name} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:description" content={description} />
<meta property="og:site_name" content={siteConfig.name} />
<meta property="og:locale" content="EN_US" />
</svelte:head>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Moon, Sun } from 'lucide-svelte';
import { Button } from '../ui/button';
import * as DropdownMenu from '../ui/dropdown-menu';
import { resetMode, setMode } from 'mode-watcher';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} variant="ghost" class="h-9 w-9">
<Sun
class="absolute h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Moon
class="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item on:click={() => setMode('light')}>Light</DropdownMenu.Item>
<DropdownMenu.Item on:click={() => setMode('dark')}>Dark</DropdownMenu.Item>
<DropdownMenu.Item on:click={() => resetMode()}>System</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,2 @@
export { default as MainNav } from './main-nav.svelte';
export { default as MobileNav } from './mobile-nav.svelte';

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { page } from '$app/stores';
import { cn } from '$lib/utils';
import { navConfig } from '$lib/config/nav';
export let authenticated = false;
</script>
<div class="mr-4 hidden md:flex">
<nav class="flex items-center space-x-6 text-sm font-medium">
{#each navConfig.mainNav as navItem, index (navItem + index.toString())}
{#if navItem.href && (navItem.auth == authenticated || navItem.always)}
<a
href={navItem.href}
class={cn(
'transition-colors hover:text-foreground/80',
$page.url.pathname === navItem.href ? 'text-foreground' : 'text-foreground/60'
)}
>
{navItem.title}
</a>
{/if}
{/each}
</nav>
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { page } from '$app/stores';
import { cn } from '$lib/utils';
export let href: string;
export let open: boolean;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<a
{href}
on:click={() => (open = false)}
class={cn(
$page.url.pathname === href ? 'text-foreground' : 'text-foreground/60',
'hover:text-foreground',
className
)}
{...$$restProps}
>
<slot />
</a>

View file

@ -0,0 +1,68 @@
<script lang="ts">
import * as Sheet from '$lib/components/ui/sheet/';
import { HamburgerMenu } from 'radix-icons-svelte';
import { Button } from '$lib/components/ui/button';
import { navConfig } from '$lib/config/nav';
import { siteConfig } from '$lib/config/site';
import { Icons } from '../icons';
import MobileLink from './mobile-link.svelte';
let open = false;
export let authenticated = false;
</script>
<Sheet.Root bind:open>
<Sheet.Trigger asChild let:builder>
<Button
builders={[builder]}
variant="ghost"
class="mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
>
<HamburgerMenu class="h-5 w-5" />
<span class="sr-only">Toggle Menu</span>
</Button>
</Sheet.Trigger>
<Sheet.Content side="right" class="pr-0">
<MobileLink href="/" class="flex items-center" bind:open>
<span class="sr-only">Logo icon (return home)</span>
<div class="mr-4 rounded-sm bg-gray-950 p-0.5 dark:bg-transparent">
<Icons.logo />
</div>
<span class="font-mono font-bold tracking-tighter">{siteConfig.name}</span>
</MobileLink>
<div class="my-4 h-[calc(100vh-8rem)] overflow-auto pl-1 pt-10">
<div class="flex flex-col space-y-3">
{#each navConfig.mainNav as navItem, index (navItem + index.toString())}
{#if navItem.href && (navItem.auth == authenticated || navItem.always)}
<MobileLink href={navItem.href} bind:open class="pt-2 text-5xl font-bold">
{navItem.title}
</MobileLink>
{/if}
{/each}
</div>
<div class="flex flex-col space-y-2">
{#each navConfig.sidebarNav as navItem, index (index)}
<div class="flex flex-col space-y-3 pt-6">
<h4 class="font-medium">{navItem.title}</h4>
{#if navItem?.items?.length}
{#each navItem.items as item}
{#if !item.disabled && item.href}
<MobileLink href={item.href} bind:open class="text-muted-foreground">
{item.title}
{#if item.label}
<span
class="ml-2 rounded-md bg-[#adfa1d] px-1.5 py-0.5 text-xs leading-none text-[#000000]"
>
{item.label}
</span>
{/if}
</MobileLink>
{/if}
{/each}
{/if}
</div>
{/each}
</div>
</div>
</Sheet.Content>
</Sheet.Root>

View file

@ -0,0 +1,49 @@
<script lang="ts">
import * as Avatar from '$lib/components/ui/avatar';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import type { BaseAuthStore } from 'pocketbase';
export let authenticated = false;
export let user: BaseAuthStore['model'];
</script>
{#if authenticated}
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button variant="ghost" builders={[builder]} class="relative h-8 w-8 rounded-full">
<Avatar.Root class="h-9 w-9">
<Avatar.Image src={user?.avatarUrl} alt={user?.name} />
<Avatar.Fallback>{user?.initials}</Avatar.Fallback>
</Avatar.Root>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56" align="end">
<DropdownMenu.Label class="font-normal">
<div class="flex flex-col space-y-1">
<p class="text-sm font-medium leading-none">{user?.name || user?.username}</p>
<p class="text-xs leading-none text-muted-foreground">{user?.email}</p>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item>Dashboards</DropdownMenu.Item>
<DropdownMenu.Item>Connectors</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Label class="text-xs leading-none text-muted-foreground">
Settings
</DropdownMenu.Label>
<DropdownMenu.Group>
<DropdownMenu.Item href="/settings">Profile</DropdownMenu.Item>
<DropdownMenu.Item href="/settings/appearance">Appearance</DropdownMenu.Item>
<DropdownMenu.Item href="/settings/notifications">Notifications</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/logout">
Log out
<DropdownMenu.Shortcut>⇧⌘Q</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
{:else}
<Button href="/auth">Login</Button>
{/if}

View file

@ -0,0 +1,248 @@
<script>
import { mode } from 'mode-watcher';
import { onMount, beforeUpdate, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
const mousePositionStore = writable({ x: 0, y: 0 });
let x = 0;
let y = 0;
const handleMouseMove = (/** @type {{ clientX: number; clientY: number; }} */ event) => {
x = event.clientX;
y = event.clientY;
mousePositionStore.set({ x, y });
};
export let className = 'h-full';
export let quantity = 30;
export let staticity = 50;
export let ease = 50;
export let vx = 0;
export let vy = 0;
let color = '#ffffff';
let rgb = hexToRgb(color);
/**
* @type {HTMLCanvasElement}
*/
let canvasRef;
/**
* @type {HTMLDivElement}
*/
let canvasContainerRef;
/**
* @type {CanvasRenderingContext2D | null}
*/
let context;
/**
* @type {any[]}
*/
let circles = [];
let mousePosition = mousePositionStore;
let mouse = { x: 0, y: 0 };
let canvasSize = { w: 0, h: 0 };
let dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
/**
* @param {string} hex
*/
function hexToRgb(hex) {
hex = hex.replace('#', '');
const hexInt = parseInt(hex, 16);
const red = (hexInt >> 16) & 255;
const green = (hexInt >> 8) & 255;
const blue = hexInt & 255;
return [red, green, blue];
}
mode.subscribe((value) => {
color = value === 'dark' ? '#ffffff' : '#000000';
rgb = hexToRgb(color);
});
/**
* @param {{ x: any; y: any; translateX: any; translateY: any; size: any; alpha: any; targetAlpha?: number; dx?: number; dy?: number; magnetism?: number; }} circle
*/
function drawCircle(circle, update = false) {
if (context) {
const { x, y, translateX, translateY, size, alpha } = circle;
context.translate(translateX, translateY);
context.beginPath();
context.arc(x, y, size, 0, 2 * Math.PI);
context.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`;
context.fill();
context.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!update) {
circles.push(circle);
}
}
}
function initCanvas() {
resizeCanvas();
drawParticles();
}
function onMouseMove() {
if (canvasRef) {
const rect = canvasRef.getBoundingClientRect();
const { w, h } = canvasSize;
const x = $mousePosition.x - rect.left - w / 2;
const y = $mousePosition.y - rect.top - h / 2;
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
if (inside) {
mouse.x = x;
mouse.y = y;
}
}
}
function resizeCanvas() {
if (canvasContainerRef && canvasRef && context) {
circles = [];
canvasSize.w = canvasContainerRef.offsetWidth;
canvasSize.h = canvasContainerRef.offsetHeight;
canvasRef.width = canvasSize.w * dpr;
canvasRef.height = canvasSize.h * dpr;
canvasRef.style.width = `${canvasSize.w}px`;
canvasRef.style.height = `${canvasSize.h}px`;
context.scale(dpr, dpr);
}
}
function circleParams() {
const x = Math.floor(Math.random() * canvasSize.w);
const y = Math.floor(Math.random() * canvasSize.h);
const translateX = 0;
const translateY = 0;
const size = Math.floor(Math.random() * 2) + 1;
const alpha = 0;
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
const dx = (Math.random() - 0.5) * 0.2;
const dy = (Math.random() - 0.5) * 0.2;
const magnetism = 0.1 + Math.random() * 4;
return {
x,
y,
translateX,
translateY,
size,
alpha,
targetAlpha,
dx,
dy,
magnetism
};
}
function clearContext() {
if (context) {
context.clearRect(0, 0, canvasSize.w, canvasSize.h);
}
}
function drawParticles() {
clearContext();
const particleCount = quantity;
for (let i = 0; i < particleCount; i++) {
const circle = circleParams();
drawCircle(circle);
}
}
/**
* @param {number} value
* @param {number} start1
* @param {number} end1
* @param {number} start2
* @param {number} end2
*/
function remapValue(value, start1, end1, start2, end2) {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
}
function animate() {
clearContext();
circles.forEach((circle, i) => {
const edge = [
circle.x + circle.translateX - circle.size,
canvasSize.w - circle.x - circle.translateX - circle.size,
circle.y + circle.translateY - circle.size,
canvasSize.h - circle.y - circle.translateY - circle.size
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
if (remapClosestEdge > 1) {
circle.alpha += 0.02;
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha;
}
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
}
circle.x += circle.dx + vx;
circle.y += circle.dy + vy;
circle.translateX += (mouse.x / (staticity / circle.magnetism) - circle.translateX) / ease;
circle.translateY += (mouse.y / (staticity / circle.magnetism) - circle.translateY) / ease;
if (
circle.x < -circle.size ||
circle.x > canvasSize.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.h + circle.size
) {
circles.splice(i, 1);
const newCircle = circleParams();
drawCircle(newCircle);
} else {
drawCircle(
{
...circle,
x: circle.x,
y: circle.y,
translateX: circle.translateX,
translateY: circle.translateY,
alpha: circle.alpha
},
true
);
}
});
setTimeout(() => {
requestAnimationFrame(animate);
}, 1000 / 60); // Limit the frame rate to 60 FPS
}
onMount(() => {
window.addEventListener('mousemove', handleMouseMove);
if (canvasRef) {
context = canvasRef?.getContext('2d');
}
initCanvas();
animate();
window.addEventListener('resize', initCanvas);
return () => {
window.removeEventListener('resize', initCanvas);
window.removeEventListener('mousemove', handleMouseMove);
};
});
beforeUpdate(() => {
onMouseMove();
});
onDestroy(() => {
if (canvasRef) {
window.removeEventListener('resize', initCanvas);
}
});
</script>
<div class={className} bind:this={canvasContainerRef} aria-hidden="true">
<canvas bind:this={canvasRef}></canvas>
</div>

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