init production
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
service-account.json
|
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
BIN
build.zip
Normal file
14
components.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
},
|
||||
"typescript": true
|
||||
}
|
5705
package-lock.json
generated
Normal file
45
package.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "firstapp",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jhubbardsf/svelte-sortablejs": "^1.1.0",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/node": "^20.12.11",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.10.3",
|
||||
"postcss": "^8.4.38",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"bits-ui": "^0.21.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"drag-drop-touch": "^1.3.1",
|
||||
"firebase": "^10.11.1",
|
||||
"firebase-admin": "^12.1.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"svelte-drag-drop-touch": "^0.1.9",
|
||||
"svelte-toasts": "^1.1.2",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
78
src/app.css
Normal file
|
@ -0,0 +1,78 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/*
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7,26.8%,83.9);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}*/
|
13
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
16
src/lib/components/AnimatedRoute.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import {
|
||||
fly
|
||||
} from "svelte/transition";
|
||||
import {
|
||||
page
|
||||
} from "$app/stores";
|
||||
</script>
|
||||
|
||||
{#key $page.url}
|
||||
|
||||
<div in:fly={{x:'-100%', duration: 300}}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{/key}
|
14
src/lib/components/AuthCheck.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { user } from "$lib/firebase";
|
||||
</script>
|
||||
|
||||
{#if $user}
|
||||
<slot />
|
||||
{:else}
|
||||
<main class="card card-body bg-neutral mx-auto">
|
||||
<p class="text-error mx-auto">
|
||||
You must be signed in to view this page.
|
||||
</p>
|
||||
<a class="btn btn-primary mt-2" href="/login">Sign in</a>
|
||||
</main>
|
||||
{/if}
|
84
src/lib/components/SortableList.svelte
Normal file
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts" src="https://unpkg.com/svelte-drag-drop-touch">
|
||||
import { flip } from "svelte/animate";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let list: any[];
|
||||
let isOver: string | boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function getDraggedParent(node: any) {
|
||||
if (!node.dataset.index) {
|
||||
return getDraggedParent(node.parentNode);
|
||||
} else {
|
||||
return { ...node.dataset };
|
||||
}
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent) {
|
||||
// @ts-ignore
|
||||
const dragged = getDraggedParent(e.target);
|
||||
e.dataTransfer?.setData("source", dragged?.index.toString());
|
||||
}
|
||||
|
||||
|
||||
function onDragOver(e: DragEvent) {
|
||||
// @ts-ignore
|
||||
const id = e.target.dataset?.id;
|
||||
const dragged = getDraggedParent(e.target);
|
||||
isOver = dragged?.id ?? false;
|
||||
}
|
||||
|
||||
function onDragLeave(e: DragEvent) {
|
||||
const dragged = getDraggedParent(e.target);
|
||||
isOver === dragged.id && (isOver = false);
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
isOver = false;
|
||||
const dragged = getDraggedParent(e.target);
|
||||
reorder({
|
||||
from: e.dataTransfer?.getData("source"),
|
||||
to: dragged.index,
|
||||
});
|
||||
}
|
||||
|
||||
const reorder = ({ from, to }: any) => {
|
||||
const newList = [...list];
|
||||
newList[from] = [newList[to], (newList[to] = newList[from])][0];
|
||||
|
||||
dispatch("sort", newList);
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
{#if list?.length}
|
||||
<ul class="list-none p-0 flex flex-col items-center w-full">
|
||||
{#each list as item, index (item.id)}
|
||||
<li
|
||||
class="border-2 border-dashed border-transparent p-1 transition-all max-w-md w-full"
|
||||
class:over={item.id === isOver}
|
||||
data-index={index}
|
||||
data-id={item.id}
|
||||
draggable="true"
|
||||
on:dragstart={onDragStart}
|
||||
on:dragover|preventDefault={onDragOver}
|
||||
on:dragleave={onDragLeave}
|
||||
on:drop|preventDefault={onDrop}
|
||||
animate:flip={{ duration: 500 }}
|
||||
>
|
||||
<slot {item} {index} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="text-center my-12 text-lg font-bold">No items</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.over {
|
||||
@apply border-gray-400 scale-105;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
12
src/lib/components/UserLink.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
export let icon = 'default';
|
||||
export let url = 'foo';
|
||||
export let title = 'some cool title';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<a href={url} class="stack w-full max-w-md text-center bg-base-300 flex justify-center items-center p-4 rounded-lg not-prose no-underline">
|
||||
<img src={`/icons/${icon}.png`} alt={icon} width="32" height="32" class="w-8" />
|
||||
<span class="text-lg text-white font-bold">{title}</span>
|
||||
</a>
|
||||
</div>
|
16
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = AvatarPrimitive.FallbackProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</AvatarPrimitive.Fallback>
|
18
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = AvatarPrimitive.ImageProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let src: $$Props["src"] = undefined;
|
||||
export let alt: $$Props["alt"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
{src}
|
||||
{alt}
|
||||
class={cn("aspect-square h-full w-full", className)}
|
||||
{...$$restProps}
|
||||
/>
|
18
src/lib/components/ui/avatar/avatar.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = AvatarPrimitive.Props;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let delayMs: $$Props["delayMs"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
{delayMs}
|
||||
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</AvatarPrimitive.Root>
|
13
src/lib/components/ui/avatar/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Root from "./avatar.svelte";
|
||||
import Image from "./avatar-image.svelte";
|
||||
import Fallback from "./avatar-fallback.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
};
|
1
src/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
0
src/lib/server/admin.ts
Normal file
62
src/lib/utils.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import type { TransitionConfig } from "svelte/transition";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
type FlyAndScaleParams = {
|
||||
y?: number;
|
||||
x?: number;
|
||||
start?: number;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export const flyAndScale = (
|
||||
node: Element,
|
||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||
): TransitionConfig => {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
|
||||
const scaleConversion = (
|
||||
valueA: number,
|
||||
scaleA: [number, number],
|
||||
scaleB: [number, number]
|
||||
) => {
|
||||
const [minA, maxA] = scaleA;
|
||||
const [minB, maxB] = scaleB;
|
||||
|
||||
const percentage = (valueA - minA) / (maxA - minA);
|
||||
const valueB = percentage * (maxB - minB) + minB;
|
||||
|
||||
return valueB;
|
||||
};
|
||||
|
||||
const styleToString = (
|
||||
style: Record<string, number | string | undefined>
|
||||
): string => {
|
||||
return Object.keys(style).reduce((str, key) => {
|
||||
if (style[key] === undefined) return str;
|
||||
return str + `${key}:${style[key]};`;
|
||||
}, "");
|
||||
};
|
||||
|
||||
return {
|
||||
duration: params.duration ?? 200,
|
||||
delay: 0,
|
||||
css: (t) => {
|
||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||
|
||||
return styleToString({
|
||||
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||
opacity: t
|
||||
});
|
||||
},
|
||||
easing: cubicOut
|
||||
};
|
||||
};
|
12
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import "../app.css"
|
||||
import {user, userData} from '$lib/firebase'
|
||||
|
||||
$user
|
||||
$userData
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
|
4
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<a href="/login" class="btn btn-outline mx-auto my-auto">/Start</a>
|
97
src/routes/[username]/+page.svelte
Normal file
|
@ -0,0 +1,97 @@
|
|||
<script lang="ts">
|
||||
import UserLink from "$lib/components/UserLink.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { userData, auth } from "$lib/firebase";
|
||||
import {
|
||||
signInWithPopup,
|
||||
GoogleAuthProvider,
|
||||
signOut,
|
||||
} from "firebase/auth";
|
||||
import { goto } from "$app/navigation";
|
||||
import { toasts, ToastContainer, FlatToast } from "svelte-toasts";
|
||||
|
||||
export let data: PageData;
|
||||
async function signInWithGoogle() {
|
||||
const provider = new GoogleAuthProvider();
|
||||
const user = await signInWithPopup(auth, provider);
|
||||
console.log(user);
|
||||
}
|
||||
async function signOutSSR() {
|
||||
await signOut(auth)
|
||||
goto('/')
|
||||
}
|
||||
const showToast = () => {
|
||||
const toast = toasts.add({
|
||||
description: 'Copied!',
|
||||
duration: 3000, // 0 or negative to avoid auto-remove
|
||||
placement: 'bottom-center',
|
||||
theme: 'dark',
|
||||
type: 'success',
|
||||
onClick: () => {},
|
||||
// component: BootstrapToast, // allows to override toast component/template per toast
|
||||
});
|
||||
};
|
||||
function copyURL() {
|
||||
const copytext = `https://nmd.mov/${$userData?.username}`
|
||||
navigator.clipboard.writeText(copytext)
|
||||
showToast()
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>@{data.username} Links</title>
|
||||
<meta name="description" content="={data.bio}" />
|
||||
</svelte:head>
|
||||
|
||||
<main class=" prose text-center mx-auto mt-8">
|
||||
<h1 class="text-4xl text-teal-700">
|
||||
@{data.username}
|
||||
</h1>
|
||||
<img
|
||||
src={data.photoURL ?? "/user.png"}
|
||||
alt="photoURL"
|
||||
width="128"
|
||||
class="mx-auto rounded-2xl mt-8"
|
||||
/>
|
||||
<p class="text-xl mx-auto my-8 whitespace-pre-line">
|
||||
{data.bio ?? "no bio yet..."}
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<div class="mx-auto">
|
||||
<ul class=" list-none min-w-96">
|
||||
{#each data.links as item}
|
||||
<div class="my-2">
|
||||
<UserLink {...item} />
|
||||
</div>
|
||||
{/each}
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
{#if $userData?.username == data.username}
|
||||
<div class=" top-0 right-0 absolute">
|
||||
<a href="/{$userData?.username}/edit">
|
||||
<button class="btn btn-ghost text-xs btn-xs mx-3 my-3"
|
||||
>Edit Profile</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-error text-xs btn-xs mx-3 my-3"
|
||||
on:click={signOutSSR}>Sign Out</button
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mx-auto">
|
||||
<button on:click={copyURL} class="mx-auto btn btn-neutral">share</button>
|
||||
<ToastContainer let:data={data}>
|
||||
<FlatToast {data} />
|
||||
</ToastContainer>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="top-0 right-0 absolute">
|
||||
<button
|
||||
class="btn btn-success text-xs btn-xs mx-3 my-3"
|
||||
on:click={signInWithGoogle}>Sign in</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
33
src/routes/[username]/+page.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { collection, getDocs, limit, query, where } from 'firebase/firestore';
|
||||
import type { PageLoad } from './$types';
|
||||
import { db } from '$lib/firebase';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
|
||||
const collectionRef = collection(db, "users")
|
||||
|
||||
const q = query(
|
||||
collectionRef,
|
||||
where("username", "==", params.username),
|
||||
limit(1)
|
||||
)
|
||||
const snapshot = await getDocs(q)
|
||||
const exists = snapshot.docs[0]?.exists()
|
||||
const data = snapshot.docs[0]?.data()
|
||||
|
||||
if (!exists) {
|
||||
throw error(404, "that user does not exists")
|
||||
}
|
||||
|
||||
if (!data.published) {
|
||||
throw error(403, `The Profile of @${data.username} is not public!`)
|
||||
}
|
||||
|
||||
return {
|
||||
username: data.username,
|
||||
photoURL: data.photoURL,
|
||||
bio: data.bio,
|
||||
links: data.links ?? [],
|
||||
}
|
||||
}) satisfies PageLoad;
|
252
src/routes/[username]/edit/+page.svelte
Normal file
|
@ -0,0 +1,252 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import { db, user, userData } from "$lib/firebase";
|
||||
import {
|
||||
arrayRemove,
|
||||
arrayUnion,
|
||||
doc,
|
||||
setDoc,
|
||||
updateDoc,
|
||||
} from "firebase/firestore";
|
||||
import { writable } from "svelte/store";
|
||||
import SortableList from "$lib/components/SortableList.svelte";
|
||||
//import { SortableList } from "@jhubbardsf/svelte-sortablejs"
|
||||
import UserLink from "$lib/components/UserLink.svelte";
|
||||
import * as Avatar from "$lib/components/ui/avatar";
|
||||
|
||||
|
||||
|
||||
const icons = ["X", "Youtube", "TikTok", "LinkedIn", "GitHub", "Instagram", "Custom"];
|
||||
|
||||
const formDefaults = {
|
||||
icon: "custom",
|
||||
title: "",
|
||||
url: "https://",
|
||||
};
|
||||
|
||||
const formData = writable(formDefaults);
|
||||
let showForm = false;
|
||||
var bio = writable("");
|
||||
|
||||
$: urlIsValid = $formData.url.match(/^(ftp|http|https):\/\/[^ "]+$/);
|
||||
$: titleIsValid = $formData.title.length < 20 && $formData.title.length > 0;
|
||||
$: formIsValid = urlIsValid && titleIsValid;
|
||||
|
||||
async function addLink(e: SubmitEvent) {
|
||||
const userRef = doc(db, "users", $user!.uid);
|
||||
|
||||
await updateDoc(userRef, {
|
||||
links: arrayUnion({
|
||||
...$formData,
|
||||
id: Date.now().toString(),
|
||||
}),
|
||||
});
|
||||
|
||||
formData.set({
|
||||
icon: "",
|
||||
title: "",
|
||||
url: "",
|
||||
});
|
||||
|
||||
showForm = false;
|
||||
}
|
||||
|
||||
async function deleteLink(item: any) {
|
||||
const userRef = doc(db, "users", $user!.uid);
|
||||
await updateDoc(userRef, {
|
||||
links: arrayRemove(item),
|
||||
});
|
||||
}
|
||||
|
||||
let editing = false
|
||||
function editLink(item: any) {
|
||||
const userRef = doc(db, "users", $user!.uid)
|
||||
showForm = true
|
||||
editing = true
|
||||
deleteLink(item)
|
||||
formData.set(item)
|
||||
}
|
||||
|
||||
async function submitEdit () {
|
||||
const userRef = doc(db, "users", $user!.uid);
|
||||
editing = false
|
||||
}
|
||||
|
||||
function cancelLink() {
|
||||
formData.set(formDefaults);
|
||||
showForm = false;
|
||||
}
|
||||
|
||||
function sortList(e: CustomEvent) {
|
||||
const newList = e.detail;
|
||||
const userRef = doc(db, "users", $user!.uid);
|
||||
setDoc(userRef, { links: newList }, { merge: true });
|
||||
console.log(newList)
|
||||
}
|
||||
|
||||
function updateBio(e: string) {
|
||||
const userRef = doc(db, "users", $user!.uid);
|
||||
updateDoc(userRef, {
|
||||
bio: e,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function moveUp(index: any) {
|
||||
const userRef = doc(db, "users", $user!.uid)
|
||||
const oldlist = $userData?.links
|
||||
const newList = [...oldlist!];
|
||||
newList[index] = [newList[index-1], (newList[index-1] = newList[index])][0];
|
||||
|
||||
setDoc(userRef, { links: newList }, {merge: true});
|
||||
}
|
||||
|
||||
function moveDown(index: any) {
|
||||
const oldlist = $userData?.links
|
||||
const newList = [...oldlist!];
|
||||
newList[index] = [newList[index+1], (newList[index+1] = newList[index])][0];
|
||||
|
||||
const userRef = doc(db, "users", $user!.uid)
|
||||
setDoc(userRef, { links: newList }, {merge: true});
|
||||
|
||||
}
|
||||
|
||||
//const loggedin = $userData?.username == $page.params.username;
|
||||
const loggedin = $userData?.username == $userData?.username;
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<main class="mx-auto min-w-96 max-w-xl">
|
||||
{#if loggedin}
|
||||
<div class="relative mx-auto">
|
||||
<h1 class="text-3xl font-bold mt-8 mb-4 text-center">
|
||||
Edit your Profile
|
||||
</h1>
|
||||
<a href="/login/photo" class="absolute -top-4 transition-all">
|
||||
<Avatar.Root class="size-14">
|
||||
<Avatar.Image src="{$userData?.photoURL}" alt="profile photo" />
|
||||
<Avatar.Fallback>Profile</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</a>
|
||||
</div>
|
||||
<SortableList list={$userData?.links} on:sort={sortList} let:item let:index >
|
||||
<li class="group relative mx-auto max-w-80" >
|
||||
<UserLink {...item} />
|
||||
<button
|
||||
on:click={() => deleteLink(item)}
|
||||
class="btn btn-xs btn-error group-hover:visible transition-all absolute -right-6 bottom-10"
|
||||
>Delete</button
|
||||
>
|
||||
<button
|
||||
on:click={() => editLink(item)}
|
||||
class="btn btn-xs btn-primary group-hover:visible transition-all absolute -right-6 top-10 min-w-14"
|
||||
>Edit</button>
|
||||
<button
|
||||
on:click={() => moveUp(index)}
|
||||
class="btn btn-xs btn-accent group-hover:visible transition-all absolute -left-6 bottom-10"
|
||||
>☝️</button>
|
||||
<button
|
||||
on:click={() => moveDown(index)}
|
||||
class="btn btn-xs btn-error group-hover:visible transition-all absolute -left-6 top-10 z-50"
|
||||
>👇</button>
|
||||
</li>
|
||||
</SortableList>
|
||||
|
||||
{#if showForm}
|
||||
<form
|
||||
on:submit|preventDefault={addLink}
|
||||
class="bg-base-200 p-10 w-full mx-auto rounded-xl"
|
||||
>
|
||||
<select
|
||||
name="icon"
|
||||
class="select select-sm"
|
||||
bind:value={$formData.icon}
|
||||
>
|
||||
{#each icons as icon}
|
||||
<option value={icon.toLowerCase()}>{icon}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input
|
||||
name="title"
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
class="input input-sm"
|
||||
bind:value={$formData.title}
|
||||
/>
|
||||
<div class=" my-2">
|
||||
<input
|
||||
name="url"
|
||||
type="text"
|
||||
placeholder="URL"
|
||||
class="input input-sm w-full"
|
||||
bind:value={$formData.url}
|
||||
/>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
{#if !titleIsValid}
|
||||
<p class="text-error text-xs">Must have valid title</p>
|
||||
{/if}
|
||||
{#if !urlIsValid}
|
||||
<p class="text-error text-xs">Must have a valid URL</p>
|
||||
{/if}
|
||||
{#if formIsValid}
|
||||
<p class="text-success text-xs">Looks good!</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if editing}
|
||||
<button
|
||||
disabled={!formIsValid}
|
||||
type="submit"
|
||||
on:click={submitEdit}
|
||||
class="btn btn-success block"
|
||||
>Confirm
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs my-4 btn-disabled" on:click={cancelLink}
|
||||
>Cancel</button>
|
||||
{:else}
|
||||
<button
|
||||
disabled={!formIsValid}
|
||||
type="submit"
|
||||
class="btn btn-success block"
|
||||
>Add Link
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs my-4" on:click={cancelLink}
|
||||
>Cancel</button>
|
||||
{/if}
|
||||
</form>
|
||||
{:else}
|
||||
<button
|
||||
on:click={() => (showForm = true)}
|
||||
class="btn btn-outline btn-info block mx-auto my-4"
|
||||
>
|
||||
Add a Link
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !showForm && loggedin}
|
||||
<div class="mx-auto group relative content-center">
|
||||
<textarea
|
||||
placeholder={$userData?.bio}
|
||||
class="textarea textarea-bordered text-center mx-auto w-full min-h-40 border whitespace-pre-line"
|
||||
bind:value={$bio}
|
||||
on:input={updateBio($bio)}
|
||||
/>
|
||||
<button
|
||||
on:click={() => updateBio($bio)}
|
||||
class="btn btn-xs btn-success invisible group-hover:visible transition-all absolute -right-6 bottom-10"
|
||||
>Save</button
|
||||
>
|
||||
</div>
|
||||
<a href="/{$userData?.username}/">
|
||||
<button class="btn btn-success my-4 block mx-auto w-3/4">Update</button>
|
||||
</a>
|
||||
{/if}
|
||||
{#if !loggedin}
|
||||
<p class="text-error mx-auto my-auto">You Must Be Signed In</p>
|
||||
<a href="/login">
|
||||
<button class="btn btn-primary my-4 block mx-auto w-3/4">Login</button>
|
||||
</a>
|
||||
{/if}
|
||||
</main>
|
32
src/routes/login/+layout.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import AnimatedRoute from "../../lib/components/AnimatedRoute.svelte";
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<nav class="flex justify-center my-10">
|
||||
<ul class="steps">
|
||||
<a href="/login" class="step step-primary">Sign In</a>
|
||||
<a
|
||||
href="/login/username"
|
||||
class="step"
|
||||
class:step-primary={$page.route.id?.match(/username|photo/g)}>
|
||||
Choose Username
|
||||
</a>
|
||||
<a
|
||||
href="/login/photo"
|
||||
class="step"
|
||||
class:step-primary={$page.route.id?.includes("photo")}>
|
||||
Upload Photo
|
||||
</a>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<AnimatedRoute>
|
||||
<main class="card w-4/6 bg-neutral text-neutral-content mx-auto">
|
||||
<div class="card-body items-center text-center">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
</AnimatedRoute>
|
33
src/routes/login/+page.svelte
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { auth} from "$lib/firebase";
|
||||
import {
|
||||
GoogleAuthProvider,
|
||||
signOut,
|
||||
signInWithPopup,
|
||||
} from "firebase/auth";
|
||||
import { user } from "$lib/firebase";
|
||||
|
||||
|
||||
async function signInWithGoogle() {
|
||||
const provider = new GoogleAuthProvider();
|
||||
const user = await signInWithPopup(auth, provider);
|
||||
}
|
||||
console.log(user);
|
||||
</script>
|
||||
|
||||
<!---<h2 class=" card-title stat-title">Login</h2>-->
|
||||
|
||||
{#if $user}
|
||||
<h2 class="card-title">Welcome, {$user.displayName}</h2>
|
||||
<p class="text-center text-success">You are logged in</p>
|
||||
<!--- <button class="btn btn-warning" on:click={() => signOut(auth)}
|
||||
>Sign out</button
|
||||
> -->
|
||||
<a href="login/username">
|
||||
<button class="btn btn-primary btn-success">Next</button>
|
||||
</a>
|
||||
{:else}
|
||||
<button class="btn btn-primary" on:click={signInWithGoogle}
|
||||
>Sign in with Google</button
|
||||
>
|
||||
{/if}
|
57
src/routes/login/photo/+page.svelte
Normal file
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import AuthCheck from "$lib/components/AuthCheck.svelte";
|
||||
import { user, userData, storage, db } from "$lib/firebase";
|
||||
import { doc, updateDoc } from "firebase/firestore";
|
||||
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";
|
||||
|
||||
let previewURL: string
|
||||
let uploading = false
|
||||
$: href = `/${$userData?.username}/edit`
|
||||
|
||||
async function upload(e: any) {
|
||||
uploading = true
|
||||
const file = e.target.files[0]
|
||||
previewURL = URL.createObjectURL(file)
|
||||
const storageRef = ref(storage, `users/${$user!.uid}/profile.pngs`)
|
||||
const result = await uploadBytes(storageRef, file)
|
||||
const url = await getDownloadURL(result.ref)
|
||||
|
||||
await updateDoc(doc(db, "users", $user!.uid), { photoURL: url})
|
||||
uploading = false
|
||||
}
|
||||
[]
|
||||
</script>
|
||||
|
||||
<AuthCheck>
|
||||
<h2 class=" card-title"> Upload a Profile Photo</h2>
|
||||
|
||||
<form class=" max-w-screen-md w-full">
|
||||
<div class=" form-control w-full max-w-xs my-10 mx-auto text-center">
|
||||
<img
|
||||
src={previewURL ?? $userData?.photoURL ?? "/user.png"}
|
||||
alt="photoURL"
|
||||
width="256"
|
||||
height="256"
|
||||
class="mx-auto"
|
||||
/>
|
||||
<label for="photoURL" class="">
|
||||
<span class="">Pick a file</span>
|
||||
</label>
|
||||
<input
|
||||
on:change={upload}
|
||||
name="photoURL"
|
||||
type="file"
|
||||
class=" file-input file-input-bordered w-full max-w-xs"
|
||||
accept="image/png, image/jpeg, image/gif, image/webp"
|
||||
/>
|
||||
{#if uploading}
|
||||
<p>Uploading...</p>
|
||||
<progress class="progress progress-info w-56 mt-6 self-center"></progress>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#if !uploading}
|
||||
<a {href} class="btn btn-primary"> Finish</a>
|
||||
{/if}
|
||||
</AuthCheck>
|
107
src/routes/login/username/+page.svelte
Normal file
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts">
|
||||
import AuthCheck from "../../../lib/components/AuthCheck.svelte";
|
||||
import { db, user, userData } from "$lib/firebase";
|
||||
import { doc, getDoc, writeBatch} from "firebase/firestore";
|
||||
|
||||
let username = "";
|
||||
let loading = false;
|
||||
let isAvailable = false;
|
||||
let isLowercase = false;
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
|
||||
const re = /^(?=[a-zA-Z0-9._]{3,16}$)(?!.*[_.]{2})[^_.].*[^_.]$/;
|
||||
|
||||
$: isValid = username.length > 3 && username.length < 16 && re.test(username);
|
||||
$: isTouched = username.length > 0;
|
||||
$: isTaken = isValid && !isAvailable && !loading
|
||||
|
||||
async function checkAvailability() {
|
||||
isAvailable = false;
|
||||
clearTimeout(debounceTimer);
|
||||
if (username = username.toLowerCase())
|
||||
isLowercase
|
||||
|
||||
loading = true;
|
||||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
console.log("checking availability of", username);
|
||||
|
||||
const ref = doc(db, "usernames", username);
|
||||
const exists = await getDoc(ref).then((doc) => doc.exists());
|
||||
|
||||
isAvailable = !exists;
|
||||
loading = false;
|
||||
|
||||
}, 500);
|
||||
|
||||
}
|
||||
|
||||
async function confirmUsername() {
|
||||
console.log("confirming username", username);
|
||||
const batch = writeBatch(db);
|
||||
batch.set(doc(db, "usernames", username.toLowerCase()), { uid: $user?.uid });
|
||||
batch.set(doc(db, "users", $user!.uid), {
|
||||
username,
|
||||
photoURL: $user?.photoURL ?? null,
|
||||
published: true,
|
||||
bio: 'Bio',
|
||||
});
|
||||
|
||||
await batch.commit();
|
||||
username = '';
|
||||
isAvailable = false;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<AuthCheck>
|
||||
{#if $userData?.username}
|
||||
<p class="text-lg">
|
||||
Your Username is <span class="text-xl text-success font-bold">@{$userData.username}</span>
|
||||
</p>
|
||||
<p class=" text-sm">Username cannot be changed</p>
|
||||
<a class="btn btn-primary" href="/login/photo">UPLOAD PROFILE IMAGE</a>
|
||||
{:else}
|
||||
<h2>Username</h2>
|
||||
<form class="w-2/5 lowercase" on:submit|preventDefault={confirmUsername}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
draggable="true"
|
||||
class="input w-full"
|
||||
bind:value={username}
|
||||
on:input={checkAvailability}
|
||||
class:input-error={(!isValid && isTouched)}
|
||||
class:input-warning={isTaken}
|
||||
class:input-success={isAvailable && isValid && !loading}
|
||||
/>
|
||||
<div class="my-4 min-h-16 px-8 w-full">
|
||||
{#if loading}
|
||||
<p class="text-secondary">Checking availability of @{username}...</p>
|
||||
{/if}
|
||||
|
||||
{#if !isValid && isTouched}
|
||||
<p class="text-error text-sm">
|
||||
must be 3-16 characters long, alphanumeric only
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if isValid && !isAvailable && !loading}
|
||||
<p class="text-warning text-sm">
|
||||
@{username} is not available
|
||||
</p>
|
||||
{/if}
|
||||
{#if isLowercase}
|
||||
<p class="text-error text-sm">must be lowercase</p>
|
||||
{/if}
|
||||
|
||||
{#if isAvailable && isValid && !isLowercase}
|
||||
<button class="btn btn-success lowercase">Confirm username @{username} </button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</AuthCheck>
|
BIN
static/favicon.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
static/favicon_io.zip
Normal file
6
static/favicon_io/about.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
This favicon was generated using the following graphics from Twitter Twemoji:
|
||||
|
||||
- Graphics Title: 1f4af.svg
|
||||
- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
|
||||
- Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f4af.svg
|
||||
- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
BIN
static/favicon_io/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/favicon_io/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
static/favicon_io/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/favicon_io/favicon-16x16.png
Normal file
After Width: | Height: | Size: 790 B |
BIN
static/favicon_io/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
static/favicon_io/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/favicon_io/favicon.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
1
static/favicon_io/site.webmanifest
Normal file
|
@ -0,0 +1 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
static/icons/custom.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
static/icons/github.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
static/icons/instagram.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
static/icons/linkedin.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
static/icons/tiktok.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
static/icons/x.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/icons/youtube.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
18
svelte.config.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import adapter from "@sveltejs/adapter-node";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter({ out: "build" }),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
68
tailwind.config.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
plugins: [require("daisyui")],
|
||||
daisyui: {
|
||||
themes: ["dark"],
|
||||
},
|
||||
safelist: ["dark"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px"
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border) / <alpha-value>)",
|
||||
input: "hsl(var(--input) / <alpha-value>)",
|
||||
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||
background: "hsl(var(--background) / <alpha-value>)",
|
||||
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans]
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
19
tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
7
vite.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
|