Merge pull request #197 from bartvdbraak/refactor/svelte-rewrite
Refactor and add features to SvelteKit project
|
@ -1,2 +0,0 @@
|
|||
ignores: ["eslint", "babel-*", "depcheck", "@types/node", "@types/react-dom", "autoprefixer", "postcss"]
|
||||
skip-missing: false
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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)$' }
|
||||
]
|
||||
}
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
|
@ -11,4 +11,4 @@ updates:
|
|||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "bartvdbraak"
|
||||
- "bartvdbraak"
|
5
.github/renovate.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"reviewers": ["bartvdbraak"]
|
||||
}
|
34
.github/workflows/gh-pages-cleanup.yaml
vendored
|
@ -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
|
152
.github/workflows/lighthouse-report.yaml
vendored
|
@ -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
|
||||
|
||||

|
||||
|
||||
- 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
|
|
@ -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
|
||||
|
@ -41,7 +35,4 @@ jobs:
|
|||
uses: wearerequired/lint-action@v2.3.0
|
||||
with:
|
||||
eslint: true
|
||||
prettier: true
|
||||
|
||||
- name: Run dependency check
|
||||
run: npx depcheck
|
||||
prettier: true
|
170
.github/workflows/unlighthouse.yaml
vendored
Normal 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
|
||||
|
||||

|
||||
|
||||
- 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
|
@ -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
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
8
LICENSE
|
@ -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.
|
||||
|
@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
|
|||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
© {new Date().getUTCFullYear()} All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
/pb_data
|
17
backend/LICENSE.md
Normal 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.
|
33
backend/pb_migrations/1706833675_updated_users.js
Normal 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)
|
||||
})
|
49
backend/pb_migrations/1706834435_updated_users.js
Normal 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)
|
||||
})
|
50
backend/pb_migrations/1707253263_updated_users.js
Normal 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
13
components.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
),
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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">
|
||||
->
|
||||
</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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 };
|
12
lib/auth.ts
|
@ -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();
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
|
@ -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+).*)"],
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
106
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
4845
pnpm-lock.yaml
13
postcss.config.cjs
Normal 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;
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -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 |
|
@ -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 |
Before Width: | Height: | Size: 172 KiB |
17
src/app.d.ts
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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;
|
||||
};
|
18
src/lib/components/site/data-indicator.svelte
Normal 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>
|
13
src/lib/components/site/icons/apple.svelte
Normal 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 |
13
src/lib/components/site/icons/bitbucket.svelte
Normal 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 |
13
src/lib/components/site/icons/discord.svelte
Normal 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 |
13
src/lib/components/site/icons/facebook.svelte
Normal 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 |
4
src/lib/components/site/icons/github.svelte
Normal 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 |
13
src/lib/components/site/icons/gitlab.svelte
Normal 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 |
13
src/lib/components/site/icons/google.svelte
Normal 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 |
35
src/lib/components/site/icons/index.ts
Normal 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
|
||||
};
|
13
src/lib/components/site/icons/instagram.svelte
Normal 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 |
24
src/lib/components/site/icons/logo.svelte
Normal 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>
|
12
src/lib/components/site/icons/microsoft.svelte
Normal 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 |
15
src/lib/components/site/icons/svelte.svelte
Normal 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 |
13
src/lib/components/site/icons/twitter.svelte
Normal 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 |
4
src/lib/components/site/icons/vercel.svelte
Normal 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 |
9
src/lib/components/site/index.ts
Normal 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';
|
36
src/lib/components/site/metadata.svelte
Normal 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>
|
25
src/lib/components/site/mode-toggle.svelte
Normal 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>
|
2
src/lib/components/site/nav/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as MainNav } from './main-nav.svelte';
|
||||
export { default as MobileNav } from './mobile-nav.svelte';
|
25
src/lib/components/site/nav/main-nav.svelte
Normal 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>
|
23
src/lib/components/site/nav/mobile-link.svelte
Normal 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>
|
68
src/lib/components/site/nav/mobile-nav.svelte
Normal 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>
|
49
src/lib/components/site/nav/user-nav.svelte
Normal 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}
|
248
src/lib/components/site/particles.svelte
Normal 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>
|