feat: homepage

This commit is contained in:
not-nullptr 2024-11-15 20:22:11 +00:00
parent 70a17df7bf
commit 0370ff3abf
17 changed files with 304 additions and 340 deletions

View File

@ -12,6 +12,7 @@
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@poppanator/sveltekit-svg": "^5.0.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",

2
src/app.d.ts vendored
View File

@ -1,3 +1,5 @@
import "@poppanator/sveltekit-svg/dist/svg";
type EventPayload = {
readonly n: string;
readonly u: Location["href"];

View File

@ -4,6 +4,12 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Host+Grotesk:ital,wght@0,300..800;1,300..800&family=Radio+Canada+Big:ital,wght@0,400..700;1,400..700&display=swap"
rel="stylesheet"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@ -7,10 +7,10 @@
@import url(@fontsource/azeret-mono/600.css);
:root {
--font-body: "Lexend", system-ui, -apple-system, BlinkMacSystemFont,
--font-body: "Host Grotesk", system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
"Helvetica Neue", sans-serif;
--font-display: "Azeret Mono", var(--font-body);
--font-display: "Radio Canada Big", var(--font-body);
--transition: linear(
0,
0.006,
@ -27,6 +27,7 @@
1.017 63.9%,
1.001
);
--shadow-panel: 0 4px 6px 0 hsla(0, 0%, 0%, 0.15);
}
@mixin light {
@ -43,19 +44,35 @@
}
@mixin dark {
--accent-bg: hsl(304, 41%, 21%);
--accent-fg: hsl(303, 73%, 81%);
--bg: hsl(0, 0%, 8%);
--bg-transparent: hsla(0, 0%, 8%, 0.8);
--fg: hsl(0, 0%, 90%);
// general
--accent: hsl(303, 73%, 81%);
// foregrounds
--fg: hsl(0, 0%, 100%);
--fg-muted: hsl(0, 0%, 50%);
--fg-muted-alt: hsl(0, 0%, 25%);
--fg-highlight: hsl(303, 64%, 65%);
--fg-failure: hsl(0, 67%, 80%);
--bg-gradient: linear-gradient(to bottom, #c800ff0c, #c800ff00),
linear-gradient(to bottom left, #0015ff0c, #0015ff00 50%),
linear-gradient(to bottom right, #ff001e18, #ff001e00 50%);
--fg-on-accent: hsl(0, 0%, 0%);
// backgrounds
--bg: hsl(220, 5%, 12%);
--bg-gradient: linear-gradient(
to bottom,
hsla(287, 100%, 50%, 0.1),
hsla(287, 100%, 50%, 0)
),
linear-gradient(
to bottom left,
hsla(235, 100%, 50%, 0.07),
hsla(235, 100%, 50%, 0) 50%
),
linear-gradient(
to bottom right,
hsla(353, 100%, 50%, 0.07),
hsla(353, 100%, 50%, 0) 50%
);
color-scheme: dark;
--bg-panel: hsl(225, 4%, 18%);
--bg-panel-accented: color-mix(in srgb, var(--accent) 12%, transparent);
--bg-separator: hsl(214, 4%, 32%);
}
@media (prefers-color-scheme: dark) {
@ -79,9 +96,10 @@
}
body {
@apply text-foreground bg-background font-body overflow-x-hidden;
@apply text-foreground font-body font-semibold overflow-x-hidden;
width: 100vw;
background: var(--bg-gradient);
background-color: var(--bg);
background-size: 100vw 100vh;
}
@ -101,4 +119,13 @@ body {
.btn-highlight {
@apply bg-accent-background text-accent-foreground border-accent-background;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-display font-semibold;
}
}

View File

@ -0,0 +1,22 @@
<svg width="1389" height="1080" viewBox="0 0 1389 1080" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M418.719 1080L0.480804 0H2.62554L420.863 1080H418.719Z" fill="url(#paint0_linear_6_220)" fill-opacity="0.1"/>
<path d="M829.044 1080L412.359 0H410.215L826.9 1080H829.044Z" fill="url(#paint1_linear_6_220)" fill-opacity="0.1"/>
<path d="M788.673 555.925L987.856 0H989.981L790.985 555.402L1064.61 827.169L1386.13 0H1388.27L1065.37 830.741L788.673 555.925Z" fill="url(#paint2_linear_6_220)" fill-opacity="0.1"/>
<defs>
<linearGradient id="paint0_linear_6_220" x1="694.377" y1="0" x2="694.377" y2="1080" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.75" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_6_220" x1="694.377" y1="0" x2="694.377" y2="1080" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.75" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_6_220" x1="694.377" y1="0" x2="694.377" y2="1080" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.75" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,54 @@
<script lang="ts">
import { onMount, type SvelteComponentTyped } from "svelte";
import Panel from "../visual/Panel.svelte";
import Logo from "../visual/svg/Logo.svelte";
import clsx from "clsx";
import { page } from "$app/stores";
import { MoonIcon, SunIcon } from "lucide-svelte";
import { theme } from "$lib/store/index.svelte";
type Props = {
items: {
name: string;
url: string;
activeMatch: (pathname: string) => boolean;
icon: any;
}[];
};
let { items }: Props = $props();
</script>
<Panel class="w-fit h-20 flex items-center gap-3">
<div
class="w-32 h-full bg-accent rounded-xl flex items-center justify-center"
>
<div class="h-5 w-full">
<Logo />
</div>
</div>
{#each items as item (item.url)}
{@const Icon = item.icon}
<a
href={item.url}
aria-label={item.name}
class={clsx(
"w-32 h-full rounded-xl flex items-center justify-center gap-3",
{
"bg-panel-accented": item.activeMatch($page.url.pathname),
},
)}
>
<Icon />
<p>{item.name}</p>
</a>
{/each}
<div class="w-0.5 bg-separator h-full"></div>
<button
onclick={theme.toggle}
class="w-14 h-full flex items-center justify-center"
>
<SunIcon class="dynadark:hidden block" />
<MoonIcon class="dynadark:block hidden" />
</button>
</Panel>

View File

@ -1,121 +1,23 @@
<script lang="ts">
import { Upload } from "lucide-svelte";
import clsx from "clsx";
import { onMount } from "svelte";
import { UploadIcon } from "lucide-svelte";
import Panel from "../visual/Panel.svelte";
let fileList = $state<FileList>();
let dragBtn = $state<HTMLButtonElement>();
type Props = {
class?: string;
};
interface Props {
files: File[] | undefined;
onupload?: () => void;
isMobile: boolean;
acceptedFormats?: string[];
}
$effect(() => {
if (!fileList) return;
files = Array.from(fileList);
});
let fileInput = $state<HTMLInputElement>();
let dragOver = $state(false);
let { files = $bindable(), onupload, isMobile, acceptedFormats }: Props = $props();
function upload() {
if (!fileInput) return;
fileInput.click();
}
onMount(() => {
const handler = (e: Event) => e.preventDefault();
if (!dragBtn) return;
dragBtn.addEventListener("dragenter", handler);
dragBtn.addEventListener("dragstart", handler);
dragBtn.addEventListener("dragend", handler);
dragBtn.addEventListener("dragleave", handler);
dragBtn.addEventListener("dragover", handler);
dragBtn.addEventListener("drag", handler);
dragBtn.addEventListener("drop", handler);
return () => {
if (!dragBtn) return;
dragBtn.removeEventListener("dragenter", handler);
dragBtn.removeEventListener("dragstart", handler);
dragBtn.removeEventListener("dragend", handler);
dragBtn.removeEventListener("dragleave", handler);
dragBtn.removeEventListener("dragover", handler);
dragBtn.removeEventListener("drag", handler);
dragBtn.removeEventListener("drop", handler);
};
});
function drop(event: DragEvent) {
event.preventDefault();
dragOver = false;
if (!event.dataTransfer) return;
if (!files) files = Array.from(event.dataTransfer.files);
else files.push(...Array.from(event.dataTransfer.files));
onupload?.();
return true;
}
function addFiles() {
if (!fileInput) return;
if (!fileInput.files) return;
if (!files) files = Array.from(fileInput.files);
else files.push(...Array.from(fileInput.files));
onupload?.();
}
const { class: classList }: Props = $props();
</script>
<button
bind:this={dragBtn}
onclick={upload}
ondragover={() => (dragOver = true)}
ondragleave={() => (dragOver = false)}
class={clsx(
"file-uploader",
"w-full h-80 flex items-center justify-center cursor-pointer",
"border-2 border-solid border-foreground-muted-alt rounded-2xl",
"hover:scale-95 hover:opacity-70 transition-all duration-150 ease-out",
{
"scale-95 opacity-70 blur-xs": dragOver,
},
)}
class:_drag-over={dragOver}
ondrop={drop}
>
<div
class="file-uploader-center flex items-center justify-center flex-col transition-all duration-150 ease-out px-8"
>
<button class={classList}>
<Panel class="flex justify-center items-center w-full h-full flex-col">
<div
class="size-16 rounded-full text-accent-foreground bg-accent-background flex items-center justify-center"
class="w-16 h-16 bg-accent rounded-full flex items-center justify-center p-4"
>
<Upload class="size-8" />
<UploadIcon class="w-full h-full text-on-accent" />
</div>
<h2 class="font-display text-2xl mt-6">
{isMobile ? "Tap" : "Drop or click"} to upload files
<h2 class="text-center text-2xl font-semibold mt-4">
Drop or click to upload
</h2>
<p class="text-foreground-muted mt-4">
All processing is done on your device. No file or size limit.
</p>
</div>
</Panel>
</button>
<input
type="file"
class="hidden"
bind:this={fileInput}
onchange={addFiles}
accept={acceptedFormats?.join(",") ?? "*"}
multiple
/>
<style>
.file-uploader:hover .file-uploader-center,
.file-uploader._drag-over .file-uploader-center {
@apply scale-105;
}
</style>

View File

@ -0,0 +1,25 @@
<script lang="ts">
type Props = {
class: string;
items: { [name: string]: string };
};
const { class: classList, items }: Props = $props();
const year = new Date().getFullYear();
const links = $derived(Object.entries(items));
</script>
<footer class={classList}>
<div
class="w-full h-full flex items-center justify-center text-muted gap-3"
>
<p>© {year} VERT.</p>
{#each links as [name, url] (name)}
<!-- bullet point -->
<p></p>
<a target="_blank" class="hover:underline" href={url}>{name}</a>
{/each}
</div>
</footer>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import type { Snippet } from "svelte";
type Props = {
class?: string;
children: Snippet<[]>;
};
const { class: classList, children }: Props = $props();
</script>
<div class="bg-panel {classList} p-3 rounded-2.5xl shadow-panel">
{@render children?.()}
</div>

View File

@ -8,20 +8,42 @@ class Files {
}
class Theme {
public dark = $state(false);
public toggle = () => {
this.dark = !this.dark;
private _dark = $state(false);
public get dark() {
return this._dark;
}
public set dark(value: boolean) {
this._dark = value;
if (!browser) return;
JSCookie.set("theme", this.dark ? "dark" : "light", {
path: "/",
sameSite: "lax",
expires: 2147483647,
});
log(["theme"], `set to ${this.dark ? "dark" : "light"}`);
if (browser) {
window.plausible("Theme set", {
props: { theme: theme.dark ? "dark" : "light" },
window.plausible("Theme set", {
props: { theme: theme.dark ? "dark" : "light" },
});
if (value) {
document.documentElement.classList.add("dark");
document.documentElement.classList.remove("light");
JSCookie.set("theme", "dark", {
path: "/",
sameSite: "lax",
expires: 2147483647,
});
} else {
document.documentElement.classList.add("light");
document.documentElement.classList.remove("dark");
JSCookie.set("theme", "light", {
path: "/",
sameSite: "lax",
expires: 2147483647,
});
}
}
public toggle = () => {
this.dark = !this.dark;
};
}

View File

@ -13,27 +13,39 @@
} from "$env/static/public";
import FancyMenu from "$lib/components/functional/FancyMenu.svelte";
import { writable } from "svelte/store";
import { MoonIcon, SunIcon } from "lucide-svelte";
import {
InfoIcon,
MoonIcon,
RefreshCwIcon,
SettingsIcon,
SunIcon,
UploadIcon,
} from "lucide-svelte";
import { browser } from "$app/environment";
import JSCookie from "js-cookie";
import { onMount } from "svelte";
import Panel from "$lib/components/visual/Panel.svelte";
import Navbar from "$lib/components/functional/Navbar.svelte";
import Footer from "$lib/components/visual/Footer.svelte";
let { children, data } = $props();
let shouldGoBack = writable(false);
let navbar = $state<HTMLDivElement>();
let hover = $state(false);
const links = $derived<
const items = $derived<
{
name: string;
url: string;
activeMatch: (pathname: string) => boolean;
icon: any;
}[]
>([
{
name: "Upload",
url: "/",
activeMatch: (pathname) => pathname === "/",
icon: UploadIcon,
},
{
name:
@ -42,11 +54,19 @@
: `Convert`,
url: "/convert",
activeMatch: (pathname) => pathname === "/convert",
icon: RefreshCwIcon,
},
{
name: "Settings",
url: "/settings",
activeMatch: (pathname) => pathname.startsWith("/settings"),
icon: SettingsIcon,
},
{
name: "About",
url: "/about",
activeMatch: (pathname) => pathname.startsWith("/about"),
icon: InfoIcon,
},
]);
@ -57,27 +77,6 @@
}
};
$effect(() => {
if (!browser) return;
if (theme.dark) {
document.documentElement.classList.add("dark");
document.documentElement.classList.remove("light");
JSCookie.set("theme", "dark", {
path: "/",
sameSite: "lax",
expires: 2147483647,
});
} else {
document.documentElement.classList.add("light");
document.documentElement.classList.remove("dark");
JSCookie.set("theme", "light", {
path: "/",
sameSite: "lax",
expires: 2147483647,
});
}
});
onMount(() => {
const mouseEnter = () => {
hover = true;
@ -104,182 +103,21 @@
></script>{/if}
</svelte:head>
<div
role="main"
class="w-full h-full max-w-screen-lg mx-auto p-4"
ondragenter={maybeNavToHome}
>
<div class="flex justify-center mb-5 lg:hidden">
<a
href="/"
class="px-4 relative h-14 mr-3 justify-center items-center bg-accent-background fill-accent-foreground rounded-xl md:hidden flex"
>
<div class="h-6 relative w-24 items-center flex justify-center">
<Logo />
{#if PUB_ENV === "nightly"}
<div
class="absolute -top-6 -left-10 px-2 py-1 w-fit bg-foreground-highlight text-accent-background rotate-[-10deg] rounded-xl"
style="font-family: Comic Sans MS, sans-serif;"
>
NIGHTLY
</div>
{/if}
</div>
</a>
</div>
<div
class="w-full max-w-screen-md p-1 border-solid border-2 rounded-2xl border-foreground-muted-alt flex mb-10 mx-auto lg:mt-5"
bind:this={navbar}
>
<div class="md:p-1">
<a
href="/"
class="px-3 relative w-full h-full mr-3 justify-center items-center bg-accent-background fill-accent-foreground rounded-xl md:flex hidden"
>
<div class="h-6 w-24 items-center flex justify-center relative">
<Logo />
{#if PUB_ENV === "nightly"}
<div
class="absolute -top-6 -left-10 px-2 py-1 w-fit bg-foreground-highlight text-accent-background rotate-[-10deg] rounded-xl"
style="font-family: Comic Sans MS, sans-serif;"
>
NIGHTLY
</div>
{/if}
</div>
</a>
</div>
<FancyMenu {links} {shouldGoBack} />
<div class="h-16 px-4 flex items-center">
<button onclick={theme.toggle} class="grid-cols-1 grid-rows-1 grid">
<!-- {#if theme.dark}
<div
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
>
<MoonIcon />
</div>
{:else}
<div
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
>
<SunIcon />
</div>
{/if} -->
{#if browser}
{#if theme.dark}
<div
in:blur={{
blurMultiplier: 1,
duration,
easing: quintOut,
scale: {
start: 0.5,
end: 1,
},
}}
out:blur={{
blurMultiplier: 1,
duration,
easing: quintOut,
scale: {
start: 1,
end: 1.5,
},
}}
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
>
<MoonIcon class="w-8" />
</div>
{:else}
<div
in:blur={{
blurMultiplier: 1,
duration,
easing: quintOut,
scale: {
start: 0.5,
end: 1,
},
}}
out:blur={{
blurMultiplier: 1,
duration,
easing: quintOut,
scale: {
start: 1,
end: 1.5,
},
}}
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
>
<SunIcon class="w-8" />
</div>
{/if}
{:else}
<div
class="w-full h-full flex items-center justify-center row-start-1 col-start-1 dynadark:hidden"
>
<SunIcon class="w-8" />
</div>
<div
class="w-full h-full hidden items-center justify-center row-start-1 col-start-1 dynadark:flex"
>
<MoonIcon class="w-8" />
</div>
{/if}
</button>
</div>
</div>
<div class="w-full max-w-screen-lg grid grid-cols-1 grid-rows-1 relative">
{#key data.pathname}
<div class="w-full">
<div
class="absolute top-0 left-0 w-full"
style={hover ? "will-change: opacity, blur, transform" : ""}
in:blur={{
duration,
easing: quintOut,
blurMultiplier: 12,
x: {
start: !$shouldGoBack ? 250 : -250,
end: 0,
},
y: {
start: 100,
end: 0,
},
scale: {
start: 0.75,
end: 1,
},
origin: "top center",
}}
out:blur={{
duration,
easing: quintOut,
blurMultiplier: 12,
x: {
start: 0,
end: !$shouldGoBack ? -250 : 250,
},
y: {
start: 0,
end: 100,
},
scale: {
start: 1,
end: 0.75,
},
origin: "top center",
}}
>
<div class="pb-20">
{@render children()}
</div>
</div>
</div>
{/key}
</div>
<div class="fixed top-8 left-0 w-full flex justify-center">
<Navbar {items} />
</div>
<div class="w-screen h-screen">
{@render children()}
</div>
<div class="-mt-14 w-full h-14">
<Footer
class="w-full h-full"
items={{
"Privacy Policy": "#",
"Source Code": "#",
"Discord Server": "#",
}}
/>
</div>

View File

@ -5,4 +5,32 @@
//
// ship fast n break things !!
// -- nullptr
import VertVBig from "$lib/assets/vert-bg.svg?component";
import Uploader from "$lib/components/functional/Uploader.svelte";
</script>
<div
class="fixed -z-50 top-0 left-0 w-full h-full flex items-center justify-center overflow-hidden"
>
<VertVBig class="fill-[--fg] opacity-50" />
</div>
<div class="w-full h-full flex items-center justify-center">
<div class="max-w-5xl w-full">
<div class="flex items-center h-[266px] gap-24">
<div class="flex-grow w-full">
<h1 class="text-6xl tracking-tight leading-[72px] mb-6">
The file converter you'll love.
</h1>
<p class="font-normal text-xl text-muted">
All processing done on your device. No file size limit, no
ads, and completely open source.
</p>
</div>
<div class="flex-grow w-full h-full">
<Uploader class="w-full h-full" />
</div>
</div>
</div>
</div>

View File

View File

View File

View File

@ -5,15 +5,21 @@ export default {
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {
colors: {
background: "var(--bg)",
backgroundColor: {
panel: "var(--bg-panel)",
"panel-accented": "var(--bg-panel-accented)",
separator: "var(--bg-separator)",
},
textColor: {
foreground: "var(--fg)",
"foreground-muted": "var(--fg-muted)",
"foreground-muted-alt": "var(--fg-muted-alt)",
"foreground-failure": "var(--fg-failure)",
"foreground-highlight": "var(--fg-highlight)",
"accent-background": "var(--accent-bg)",
"accent-foreground": "var(--accent-fg)",
muted: "var(--fg-muted)",
"on-accent": "var(--fg-on-accent)",
},
colors: {
accent: "var(--accent)",
},
boxShadow: {
panel: "var(--shadow-panel)",
},
fontFamily: {
display: "var(--font-display)",
@ -22,6 +28,9 @@ export default {
blur: {
xs: "2px",
},
borderRadius: {
"2.5xl": "1.25rem",
},
},
},

View File

@ -1,5 +1,6 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import svg from "@poppanator/sveltekit-svg";
export default defineConfig({
plugins: [
@ -17,6 +18,19 @@ export default defineConfig({
});
},
},
svg({
includePaths: ["./src/lib/assets"],
svgoOptions: {
multipass: true,
plugins: [
{
name: "preset-default",
params: { overrides: { removeViewBox: false } },
},
{ name: "removeAttrs", params: { attrs: "(fill|stroke)" } },
],
},
}),
],
optimizeDeps: {
exclude: [