feat: move structure
18
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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
web/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>
|
12
web/src/lib/components/site/site-footer.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
</script>
|
||||
|
||||
<footer class="container py-6">
|
||||
<div class="space-y-1">
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} — {siteConfig.name} by {siteConfig.author}. Licensed
|
||||
under GPL-3.0.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
28
web/src/lib/components/site/site-navbar.svelte
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { Icons, ModeToggle, MainNav, MobileNav } from '$lib/components/site';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
import UserNav from './nav/user-nav.svelte';
|
||||
|
||||
export let authenticated = false;
|
||||
export let user: object | null = null;
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-50 w-full border-b bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
<div class="container flex h-14 items-center">
|
||||
<a href="/" class="mr-6 flex items-center space-x-2">
|
||||
<span class="sr-only">Logo (return home)</span>
|
||||
<div class="rounded-sm bg-gray-950 p-0.5 dark:bg-transparent">
|
||||
<Icons.logo />
|
||||
</div>
|
||||
<span class="text-xl font-bold tracking-tight">{siteConfig.name}</span>
|
||||
</a>
|
||||
<MainNav {authenticated} />
|
||||
<div class="flex flex-1 items-center justify-end space-x-2 sm:space-x-4">
|
||||
<ModeToggle />
|
||||
<UserNav {authenticated} {user} />
|
||||
<MobileNav {authenticated} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
14
web/src/lib/components/site/tailwind-indicator.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
</script>
|
||||
|
||||
<Button variant="ghost" size="icon" class="block sm:hidden">xs</Button>
|
||||
<Button variant="ghost" size="icon" class="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden"
|
||||
>sm</Button
|
||||
>
|
||||
<Button variant="ghost" size="icon" class="hidden md:block lg:hidden xl:hidden 2xl:hidden"
|
||||
>md</Button
|
||||
>
|
||||
<Button variant="ghost" size="icon" class="hidden lg:block xl:hidden 2xl:hidden">lg</Button>
|
||||
<Button variant="ghost" size="icon" class="hidden xl:block 2xl:hidden">xl</Button>
|
||||
<Button variant="ghost" size="icon" class="hidden 2xl:block">2xl</Button>
|