init production

This commit is contained in:
nomadics9 2024-05-11 23:01:04 +03:00
commit e40a297123
53 changed files with 6923 additions and 0 deletions

11
.gitignore vendored Normal file
View 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
View file

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

38
README.md Normal file
View 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

Binary file not shown.

14
components.json Normal file
View 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

File diff suppressed because it is too large Load diff

45
package.json Normal file
View 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
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

78
src/app.css Normal file
View 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
View 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
View 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>

View 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}

View 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}

View 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>

View 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>

View 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>

View 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}
/>

View 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>

View 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
View 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
View file

62
src/lib/utils.ts Normal file
View 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
View 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
View file

@ -0,0 +1,4 @@
<script lang="ts">
</script>
<a href="/login" class="btn btn-outline mx-auto my-auto">/Start</a>

View 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}

View 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;

View 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>

View 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>

View 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}

View 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>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/favicon_io.zip Normal file

Binary file not shown.

View 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/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
static/icons/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
static/icons/instagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
static/icons/linkedin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/icons/tiktok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
static/icons/x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
static/icons/youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

18
svelte.config.js Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});