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()]
|
||||||
|
});
|
||||||
|
|