Add mobile-first styling to layouts, auth, and user pages
- Consolidated navbar with hamburger menu for mobile - Responsive styling for root/protected layouts and auth pages - Mobile-friendly account, dashboard, license-key, and setup pages
This commit is contained in:
+2
-15
@@ -3,23 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script>
|
||||
(function () {
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (
|
||||
theme === 'dark' ||
|
||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body class="m-0" data-sveltekit-preload-data="hover">
|
||||
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-0 flex-1 items-center justify-center overflow-auto">
|
||||
<div class="mx-auto w-full max-w-md px-4 py-8 sm:py-12">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,67 +23,58 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-8 p-6 dark:text-gray-100">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Account</h1>
|
||||
<div class="mx-auto w-full max-w-3xl">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">Account</h1>
|
||||
<p class="mt-1 text-sm text-slate-500 sm:text-base">Manage your profile and addresses.</p>
|
||||
|
||||
<!-- Email (read-only) -->
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Email</p>
|
||||
<p class="mt-1 text-gray-900 dark:text-gray-100">{data.user.email}</p>
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<p class="text-xs tracking-wide text-slate-500 uppercase">Email</p>
|
||||
<p class="mt-1 text-base break-words text-slate-900">{data.user.email}</p>
|
||||
</div>
|
||||
|
||||
<!-- Profile Form -->
|
||||
<section>
|
||||
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Profile</h2>
|
||||
|
||||
{#if form?.action === 'updateProfile' && form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
|
||||
Profile updated successfully.
|
||||
</div>
|
||||
{/if}
|
||||
{#if form?.action === 'updateProfile' && form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action="?/updateProfile" class="flex max-w-lg flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="firstName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
First Name
|
||||
</label>
|
||||
<form method="POST" action="?/updateProfile" class="mt-5 flex flex-col gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={data.user.profile?.firstName ?? ''}
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lastName" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Last Name
|
||||
</label>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={data.user.profile?.lastName ?? ''}
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="phoneNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Phone Number
|
||||
</label>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phoneNumber"
|
||||
@@ -91,13 +82,13 @@
|
||||
bind:value={phoneNumber}
|
||||
oninput={handlePhoneInput}
|
||||
required
|
||||
class="mt-1 w-full max-w-xs rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
class="flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 sm:w-auto sm:px-8"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
@@ -106,138 +97,112 @@
|
||||
</section>
|
||||
|
||||
<!-- Addresses -->
|
||||
<section>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Addresses</h2>
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Addresses</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-green-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-green-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
onclick={() => (showAddressForm = !showAddressForm)}
|
||||
class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
{showAddressForm ? 'Cancel' : 'Add Address'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if form?.action === 'addAddress' && form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
|
||||
Address added successfully.
|
||||
</div>
|
||||
{/if}
|
||||
{#if form?.action === 'addAddress' && form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showAddressForm}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/addAddress"
|
||||
class="mb-6 max-w-lg rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>Street</label
|
||||
>
|
||||
<form method="POST" action="?/addAddress" class="mt-5 flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="street" class="text-sm font-medium text-slate-700">Street</label>
|
||||
<input
|
||||
type="text"
|
||||
id="street"
|
||||
name="street"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>City</label
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="city" class="text-sm font-medium text-slate-700">City</label>
|
||||
<input
|
||||
type="text"
|
||||
id="city"
|
||||
name="city"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>State</label
|
||||
>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="state" class="text-sm font-medium text-slate-700">State</label>
|
||||
<input
|
||||
type="text"
|
||||
id="state"
|
||||
name="state"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label
|
||||
for="postalCode"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Postal Code
|
||||
</label>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="postalCode" class="text-sm font-medium text-slate-700"> Postal Code </label>
|
||||
<input
|
||||
type="text"
|
||||
id="postalCode"
|
||||
name="postalCode"
|
||||
required
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="country"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label
|
||||
>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="country" class="text-sm font-medium text-slate-700">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
id="country"
|
||||
name="country"
|
||||
required
|
||||
value="US"
|
||||
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
class="flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 sm:w-auto sm:px-8"
|
||||
>
|
||||
Save Address
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.user.profile.addresses as address}
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100">{address.street}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{#each data.user.profile.addresses as address (address.id)}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<p class="text-sm font-medium text-slate-900">{address.street}</p>
|
||||
<p class="mt-1 text-sm text-slate-600">
|
||||
{address.city}, {address.state}
|
||||
{address.postalCode}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
|
||||
<p class="text-sm text-slate-600">{address.country}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !showAddressForm}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No addresses on file.</p>
|
||||
<p class="mt-5 text-sm text-slate-500">No addresses on file.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -42,41 +42,74 @@
|
||||
</script>
|
||||
|
||||
<Window>
|
||||
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
|
||||
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p>
|
||||
<form method="POST" class="flex flex-col items-center gap-5">
|
||||
<h1 class="text-center text-2xl font-bold text-slate-900">
|
||||
Please finish setting up your profile
|
||||
</h1>
|
||||
|
||||
{#if data.user?.image}
|
||||
<img src={data.user?.image} class="mx-auto w-[8rem] rounded-full" alt="Profile" />
|
||||
<img
|
||||
src={data.user?.image}
|
||||
alt="Profile"
|
||||
class="h-24 w-24 rounded-full object-cover shadow-sm sm:h-28 sm:w-28"
|
||||
/>
|
||||
{:else}
|
||||
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
|
||||
<div
|
||||
class="flex h-24 w-24 items-center justify-center rounded-full bg-indigo-100 text-3xl font-semibold text-indigo-700 sm:h-28 sm:w-28"
|
||||
>
|
||||
{data.user?.name?.charAt(0)?.toUpperCase() ?? '?'}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
|
||||
<div class="flex flex-col">
|
||||
<input type="text" name="userId" value={data.user?.id} class="hidden" />
|
||||
|
||||
<p class="text-base text-slate-900">{data.user?.email}</p>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<input type="hidden" name="userId" value={data.user?.id} />
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
class="my-2"
|
||||
bind:value={firstName}
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
class="my-2"
|
||||
bind:value={lastName}
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
placeholder="Phone Number"
|
||||
class="my-2"
|
||||
bind:value={phoneNumber}
|
||||
oninput={handlePhoneInput}
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="bg-blue-300 p-2 px-4 dark:bg-gray-700 dark:text-gray-100"
|
||||
>Submit</button
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="phoneNumber" class="text-sm font-medium text-slate-700"> Phone Number </label>
|
||||
<input
|
||||
id="phoneNumber"
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
placeholder="(555) 555-5555"
|
||||
bind:value={phoneNumber}
|
||||
oninput={handlePhoneInput}
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</Window>
|
||||
|
||||
@@ -1,42 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
<div class="mx-auto w-full max-w-6xl">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
|
||||
Welcome, {data.firstName}!
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-500 sm:text-base">Here's an overview of your account.</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<a
|
||||
href="/license-keys"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
||||
href={resolve('/license-keys')}
|
||||
class="group flex flex-col gap-1 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active License Keys</p>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.activeKeys}</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">{data.totalKeys} total</p>
|
||||
<span class="text-sm font-medium text-slate-600">Active License Keys</span>
|
||||
<span class="text-3xl font-bold text-indigo-600">{data.activeKeys}</span>
|
||||
<span class="text-sm text-slate-500">{data.totalKeys} total</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/admin/proclamations"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
||||
href={resolve('/account')}
|
||||
class="group flex flex-col gap-1 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Proclamations</p>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{data.proclamationCount}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/account"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Account</p>
|
||||
<p class="mt-2 text-lg font-semibold text-gray-900 dark:text-gray-100">Manage Profile</p>
|
||||
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Edit your details</p>
|
||||
<span class="text-sm font-medium text-slate-600">Account</span>
|
||||
<span class="text-2xl font-semibold text-slate-900">Manage Profile</span>
|
||||
<span class="text-sm text-slate-500">Edit your details</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,47 +4,35 @@
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">License Keys</h1>
|
||||
<div class="mx-auto w-full max-w-6xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">License Keys</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Your active and inactive license keys.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.licenseKeys.length > 0}
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<div class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<!-- Desktop / tablet: table -->
|
||||
<table class="hidden w-full text-left text-sm sm:table">
|
||||
<thead
|
||||
class="border-b border-slate-200 bg-slate-50 text-xs tracking-wide text-slate-500 uppercase"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Key
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 font-semibold">Key</th>
|
||||
<th class="px-6 py-3 font-semibold">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
|
||||
{#each data.licenseKeys as key}
|
||||
<tr>
|
||||
<td
|
||||
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{key.key}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-700 dark:text-gray-300">
|
||||
{key.level}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap">
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{#each data.licenseKeys as key (key.key)}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-6 py-4 font-mono text-slate-800">{key.key}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active
|
||||
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200'
|
||||
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}"
|
||||
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-slate-100 text-slate-600'}"
|
||||
>
|
||||
{key.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
@@ -53,10 +41,26 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Mobile: card list -->
|
||||
<ul class="divide-y divide-slate-100 sm:hidden">
|
||||
{#each data.licenseKeys as key (key.key)}
|
||||
<li class="flex items-center justify-between gap-4 px-4 py-4">
|
||||
<span class="min-w-0 font-mono text-sm break-all text-slate-800">{key.key}</span>
|
||||
<span
|
||||
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {key.active
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-slate-100 text-slate-600'}"
|
||||
>
|
||||
{key.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400">You don't have any license keys yet.</p>
|
||||
<div class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center">
|
||||
<p class="text-sm text-slate-600">You don't have any license keys yet.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children, data } = $props<{ children: any; data: LayoutData }>();
|
||||
|
||||
const isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||
const adminModels = getAllModelConfigs();
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/dashboard', label: 'Dashboard' },
|
||||
{ href: '/account', label: 'Account' },
|
||||
{ href: '/proclamations', label: 'Proclamations' },
|
||||
{ href: '/license-keys', label: 'License Keys' }
|
||||
];
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
return page.url.pathname === href || page.url.pathname.startsWith(href + '/');
|
||||
}
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<!-- Sidebar -->
|
||||
<nav class="mt-12 mr-12 flex flex-col gap-1">
|
||||
{#each navLinks as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="rounded px-3 py-1.5 text-sm font-medium transition-colors {isActive(link.href)
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
{#if isAdmin}
|
||||
<div class="mt-4 flex flex-col gap-1">
|
||||
<a
|
||||
href="/admin"
|
||||
class="rounded px-3 py-1.5 text-sm font-medium transition-colors {isActive('/admin')
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
Admin Panel
|
||||
</a>
|
||||
<div class="ml-3 flex flex-col gap-0.5">
|
||||
{#each adminModels as model}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/' + model.slug)
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href="/admin/api-keys"
|
||||
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/api-keys')
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
|
||||
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
<!-- Page content -->
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<div class="min-h-screen bg-slate-50 text-slate-900">
|
||||
<main class="p-4 sm:p-6">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
|
||||
<Window>
|
||||
{#if $page.status === 403}
|
||||
<h1 class="dark:text-gray-100">Not Authorized</h1>
|
||||
<p class="dark:text-gray-300">
|
||||
You don't have permission to view this page. <a
|
||||
href="/"
|
||||
class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a
|
||||
>.
|
||||
<h1>Not Authorized</h1>
|
||||
<p>
|
||||
You don't have permission to view this page. <a href="/">Go home</a>.
|
||||
</p>
|
||||
{/if}
|
||||
</Window>
|
||||
|
||||
+161
-52
@@ -1,79 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
import './layout.css';
|
||||
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import './layout.css';
|
||||
|
||||
let { data, children } = $props();
|
||||
|
||||
let { children, data }: { children: Snippet; data: LayoutData } = $props();
|
||||
const session = authClient.useSession();
|
||||
let isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||
|
||||
let dark = $state(false);
|
||||
let menuOpen = $state(false);
|
||||
let adminOpen = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
dark = document.documentElement.classList.contains('dark');
|
||||
});
|
||||
const isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||
const adminModels = getAllModelConfigs();
|
||||
|
||||
function toggleTheme() {
|
||||
dark = !dark;
|
||||
if (dark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
function closeMenu() {
|
||||
menuOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<title>Rhema Wave Technologies</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Rhema Wave Technologies User Portal — manage proclamations, audio content, and solfeggio frequencies."
|
||||
/>
|
||||
<meta name="theme-color" content="#6b7280" />
|
||||
<meta property="og:title" content="Rhema Wave Technologies" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Manage proclamations, audio content, and solfeggio frequencies."
|
||||
/>
|
||||
<title>Rhema Wave User Portal</title>
|
||||
<meta name="description" content="Rhema Wave Technologies User Portal" />
|
||||
<meta property="og:title" content="Rhema Wave User Portal" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Rhema Wave Technologies" />
|
||||
<meta property="og:site_name" content="Rhema Wave User Portal" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
</svelte:head>
|
||||
|
||||
<nav
|
||||
class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
|
||||
>
|
||||
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
|
||||
<div class="flex items-center gap-4">
|
||||
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur">
|
||||
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
|
||||
<a href={resolve('/')} class="text-base font-semibold text-slate-900">Rhema Wave Portal</a>
|
||||
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300"
|
||||
type="button"
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-expanded={menuOpen}
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100"
|
||||
>
|
||||
{dark ? 'Light' : 'Dark'}
|
||||
</button>
|
||||
{#if $session.data}
|
||||
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
|
||||
<button
|
||||
class="dark:text-gray-300 dark:hover:text-gray-100"
|
||||
onclick={async () => {
|
||||
await authClient.signOut();
|
||||
window.location.href = '/';
|
||||
}}>Log Out</button
|
||||
{#if menuOpen}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
|
||||
>
|
||||
{:else}
|
||||
<span class="dark:text-gray-300"
|
||||
><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> |
|
||||
<a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="12" y2="12" /><line
|
||||
x1="4"
|
||||
x2="20"
|
||||
y1="18"
|
||||
y2="18"
|
||||
/></svg
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
|
||||
</header>
|
||||
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="fixed inset-0 top-14 z-50 overflow-y-auto border-t border-slate-200 bg-white shadow-lg"
|
||||
>
|
||||
<nav class="mx-auto max-w-6xl px-4 py-4">
|
||||
{#if $session.data?.user}
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/dashboard')}
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/account')}
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
|
||||
>
|
||||
Account
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/license-keys')}
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
|
||||
>
|
||||
License Keys
|
||||
</a>
|
||||
|
||||
{#if isAdmin}
|
||||
<div class="mt-2 border-t border-slate-200 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={adminOpen}
|
||||
onclick={() => (adminOpen = !adminOpen)}
|
||||
class="flex w-full items-center justify-between rounded-lg px-4 py-3 text-base font-semibold text-indigo-700 hover:bg-indigo-50"
|
||||
>
|
||||
<span>Admin</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:rotate-180={adminOpen}
|
||||
class="transition-transform"><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if adminOpen}
|
||||
<div class="mt-1 flex flex-col gap-1">
|
||||
{#each adminModels as model (model.slug)}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base text-slate-600 hover:bg-slate-100"
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base text-slate-600 hover:bg-slate-100"
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
onclick={closeMenu}
|
||||
class="rounded-lg px-4 py-3 text-base font-medium text-slate-800 hover:bg-slate-100"
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
onclick={closeMenu}
|
||||
class="mt-1 rounded-lg bg-indigo-600 px-4 py-3 text-center text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="min-h-screen bg-slate-50 text-slate-900">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
+22
-2
@@ -1,3 +1,23 @@
|
||||
<div class="flex flex-1 flex-col items-center justify-center">
|
||||
<h1 class="dark:text-gray-100">Hello</h1>
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
</script>
|
||||
|
||||
<div class="mx-auto w-full max-w-6xl px-4 py-12 sm:py-20">
|
||||
<div class="flex flex-col items-start gap-8">
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
|
||||
Welcome to your Rhema Wave Portal
|
||||
</h1>
|
||||
<p class="mt-4 text-base text-slate-600 sm:text-lg">
|
||||
Manage your account and license keys all in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
class="inline-flex h-12 items-center justify-center rounded-lg bg-indigo-600 px-8 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageProps } from './$types';
|
||||
import Window from '$lib/components/containers/Window.svelte';
|
||||
|
||||
let { form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-12 max-w-md dark:text-gray-100">
|
||||
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Forgot Password</h1>
|
||||
|
||||
<Window>
|
||||
{#if form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="rounded-lg bg-green-50 px-4 py-6 text-center">
|
||||
<h1 class="text-xl font-bold text-slate-900">Check your email</h1>
|
||||
<p class="mt-3 text-sm text-slate-600">
|
||||
If an account with that email exists, we've sent a password reset link.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
{#if form?.error && !form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold text-slate-900">Forgot Password</h1>
|
||||
<p class="text-sm text-slate-600">
|
||||
Enter your email and we'll send you a link to reset your password.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="email" class="text-sm font-medium text-slate-700"> Email Address </label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Send Reset Link
|
||||
</button>
|
||||
@@ -42,8 +49,9 @@
|
||||
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
class="mt-4 block text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
class="mt-6 block text-center text-sm text-indigo-600 hover:text-indigo-700"
|
||||
>
|
||||
Back to Login
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</Window>
|
||||
|
||||
+5
-91
@@ -2,95 +2,9 @@
|
||||
@plugin '@tailwindcss/forms';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
/* Use class-based dark mode instead of prefers-color-scheme */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Wireframe base — remap all non-gray colors to grayscale */
|
||||
/* Use var(--color-gray-N) so dark-mode inversion flows through */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@theme {
|
||||
--color-blue-50: var(--color-gray-100);
|
||||
--color-blue-100: var(--color-gray-200);
|
||||
--color-blue-200: var(--color-gray-300);
|
||||
--color-blue-300: var(--color-gray-400);
|
||||
--color-blue-400: var(--color-gray-500);
|
||||
--color-blue-500: var(--color-gray-500);
|
||||
--color-blue-600: var(--color-gray-600);
|
||||
--color-blue-700: var(--color-gray-700);
|
||||
--color-blue-800: var(--color-gray-800);
|
||||
--color-green-50: var(--color-gray-100);
|
||||
--color-green-100: var(--color-gray-200);
|
||||
--color-green-200: var(--color-gray-300);
|
||||
--color-green-300: var(--color-gray-400);
|
||||
--color-green-400: var(--color-gray-500);
|
||||
--color-green-500: var(--color-gray-500);
|
||||
--color-green-600: var(--color-gray-600);
|
||||
--color-green-700: var(--color-gray-700);
|
||||
--color-green-800: var(--color-gray-800);
|
||||
--color-red-50: var(--color-gray-100);
|
||||
--color-red-100: var(--color-gray-200);
|
||||
--color-red-200: var(--color-gray-300);
|
||||
--color-red-300: var(--color-gray-400);
|
||||
--color-red-400: var(--color-gray-500);
|
||||
--color-red-500: var(--color-gray-500);
|
||||
--color-red-600: var(--color-gray-600);
|
||||
--color-red-700: var(--color-gray-700);
|
||||
--color-red-800: var(--color-gray-800);
|
||||
--color-purple-50: var(--color-gray-100);
|
||||
--color-purple-100: var(--color-gray-200);
|
||||
--color-purple-200: var(--color-gray-300);
|
||||
--color-purple-300: var(--color-gray-400);
|
||||
--color-purple-400: var(--color-gray-500);
|
||||
--color-purple-500: var(--color-gray-500);
|
||||
--color-purple-600: var(--color-gray-600);
|
||||
--color-purple-700: var(--color-gray-700);
|
||||
--color-purple-800: var(--color-gray-800);
|
||||
--color-purple-900: var(--color-gray-900);
|
||||
--color-slate-50: var(--color-gray-50);
|
||||
--color-slate-100: var(--color-gray-100);
|
||||
--color-slate-200: var(--color-gray-200);
|
||||
--color-slate-300: var(--color-gray-300);
|
||||
--color-slate-400: var(--color-gray-400);
|
||||
--color-slate-500: var(--color-gray-500);
|
||||
--color-slate-600: var(--color-gray-600);
|
||||
--color-slate-700: var(--color-gray-700);
|
||||
--color-slate-800: var(--color-gray-800);
|
||||
--color-slate-900: var(--color-gray-900);
|
||||
|
||||
--radius: 0px;
|
||||
--radius-sm: 0px;
|
||||
--radius-md: 0px;
|
||||
--radius-lg: 0px;
|
||||
--radius-xl: 0px;
|
||||
--radius-2xl: 0px;
|
||||
--radius-3xl: 0px;
|
||||
|
||||
--shadow-sm: 0 0 #0000;
|
||||
--shadow: 0 0 #0000;
|
||||
--shadow-md: 0 0 #0000;
|
||||
--shadow-lg: 0 0 #0000;
|
||||
--shadow-xl: 0 0 #0000;
|
||||
--shadow-2xl: 0 0 #0000;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Dark mode form elements */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.dark input,
|
||||
.dark select,
|
||||
.dark textarea {
|
||||
background-color: #1f2937;
|
||||
color: #f9fafb;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Force-disable transitions, animations, and remaining decoration */
|
||||
/* ------------------------------------------------------------------ */
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
@layer base {
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import Window from '$lib/components/containers/Window.svelte';
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
@@ -10,7 +11,7 @@
|
||||
let verified = $derived(page.url.searchParams.get('verified') === 'true');
|
||||
|
||||
async function handleSubmit() {
|
||||
const { data, error: authError } = await authClient.signIn.email({
|
||||
const { error: authError } = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
rememberMe
|
||||
@@ -26,47 +27,88 @@
|
||||
</script>
|
||||
|
||||
<Window>
|
||||
<div class="dark:text-gray-100">
|
||||
{#if verified}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mb-4 rounded-lg bg-green-50 px-4 py-3 text-sm text-green-800" role="status">
|
||||
Your email has been verified. You can now log in.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<span class="text-red-600 dark:text-gray-300">{error}</span>
|
||||
{/if}
|
||||
<form onsubmit={handleSubmit} class="my-6 flex flex-col items-center justify-center">
|
||||
<p>Login</p>
|
||||
<input type="email" placeholder="Email Address" bind:value={email} required class="my-2" />
|
||||
<input type="password" placeholder="Password" bind:value={password} required class="my-2" />
|
||||
<button
|
||||
class="mt-4 rounded bg-blue-200 px-4 py-2 dark:bg-gray-700 dark:text-gray-100"
|
||||
type="submit">Sign In</button
|
||||
>
|
||||
</form>
|
||||
<div class="mt-2 flex flex-col items-center gap-2">
|
||||
<a
|
||||
href="/forgot-password"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>Forgot Password?</a
|
||||
>
|
||||
<a
|
||||
href="/register"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>Don't have an account? Register</a
|
||||
>
|
||||
<div class="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col items-center justify-center">
|
||||
<span>Or</span>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold text-slate-900">Login</h1>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="email" class="text-sm font-medium text-slate-700"> Email Address </label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Email Address"
|
||||
bind:value={email}
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="password" class="text-sm font-medium text-slate-700"> Password </label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="flex items-center gap-2 text-sm text-slate-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rememberMe}
|
||||
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Remember me
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-2 text-center text-sm">
|
||||
<a href={resolve('/forgot-password')} class="text-indigo-600 hover:text-indigo-700">
|
||||
Forgot Password?
|
||||
</a>
|
||||
<span class="text-slate-500">
|
||||
Don't have an account?
|
||||
<a href={resolve('/register')} class="font-medium text-indigo-600 hover:text-indigo-700">
|
||||
Register
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 border-t border-slate-200 pt-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-px flex-1 bg-slate-200"></div>
|
||||
<span class="text-sm text-slate-500">Or</span>
|
||||
<div class="h-px flex-1 bg-slate-200"></div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
await authClient.signIn.social({ provider: 'google', callbackURL: '/account' });
|
||||
}}
|
||||
class="mx-auto mt-4 flex h-11 items-center justify-center rounded-lg bg-white hover:bg-slate-50"
|
||||
>
|
||||
<img src="light/web_light_sq_SI.svg" alt="Sign In with Google" />
|
||||
<img src="light/web_light_sq_SI.svg" alt="Sign In with Google" class="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
|
||||
@@ -1,60 +1,128 @@
|
||||
<script lang="ts">
|
||||
import Window from '$lib/components/containers/Window.svelte';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<Window>
|
||||
<div class="dark:text-gray-100">
|
||||
{#if form?.success}
|
||||
<div class="mx-auto max-w-md text-center">
|
||||
<div
|
||||
class="rounded-lg border border-green-200 bg-green-50 p-6 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<p class="text-lg font-medium">Account created!</p>
|
||||
<p class="mt-2">
|
||||
We've sent a verification email. Check your inbox and click the link to verify your
|
||||
email address, then log in.
|
||||
<div class="rounded-lg bg-green-50 px-4 py-6 text-center">
|
||||
<h1 class="text-xl font-bold text-slate-900">Account created!</h1>
|
||||
<p class="mt-3 text-sm text-slate-600">
|
||||
We've sent a verification email. Check your inbox and click the link to verify your email
|
||||
address, then log in.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
class="mt-6 inline-flex h-11 items-center justify-center rounded-lg bg-indigo-600 px-6 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Go to Login
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-start justify-center">
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="mb-4 w-full rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
<form method="POST" class="flex flex-col items-start">
|
||||
<input type="text" name="firstName" placeholder="First Name" required class="mb-2" />
|
||||
<input type="text" name="lastName" placeholder="Last Name" required class="mb-2" />
|
||||
<input type="email" name="email" placeholder="Email Address" required class="mb-2" />
|
||||
<input type="password" name="password" placeholder="Password" required class="mb-2" />
|
||||
|
||||
<form method="POST" class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold text-slate-900">Create Account</h1>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="firstName" class="text-sm font-medium text-slate-700"> First Name </label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="lastName" class="text-sm font-medium text-slate-700"> Last Name </label>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="email" class="text-sm font-medium text-slate-700"> Email Address </label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="password" class="text-sm font-medium text-slate-700"> Password </label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="password2" class="text-sm font-medium text-slate-700"> Verify Password </label>
|
||||
<input
|
||||
id="password2"
|
||||
type="password"
|
||||
name="password2"
|
||||
placeholder="Verify Password"
|
||||
required
|
||||
class="mb-2"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mx-auto rounded-full bg-blue-600 px-6 py-2 text-white dark:bg-gray-700 dark:text-gray-100"
|
||||
>Submit</button
|
||||
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
<p class=" mx-auto my-6 text-center">Or</p>
|
||||
|
||||
<div class="mt-6 border-t border-slate-200 pt-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-px flex-1 bg-slate-200"></div>
|
||||
<span class="text-sm text-slate-500">Or</span>
|
||||
<div class="h-px flex-1 bg-slate-200"></div>
|
||||
</div>
|
||||
<button
|
||||
class="mx-auto"
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
await authClient.signIn.social({ provider: 'google', callbackURL: '/account/setup' });
|
||||
}}><img src="light/web_light_sq_SU.svg" alt="Sign Up with Google" /></button
|
||||
}}
|
||||
class="mt-4 flex h-11 w-full items-center justify-center rounded-lg border border-slate-300 bg-white hover:bg-slate-50"
|
||||
>
|
||||
<img src="light/web_light_sq_SU.svg" alt="Sign Up with Google" class="h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-sm text-slate-500">
|
||||
Already have an account?
|
||||
<a href={resolve('/login')} class="font-medium text-indigo-600 hover:text-indigo-700">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Window>
|
||||
|
||||
@@ -1,42 +1,53 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import Window from '$lib/components/containers/Window.svelte';
|
||||
|
||||
let { form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-12 max-w-md dark:text-gray-100">
|
||||
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Reset Password</h1>
|
||||
|
||||
<Window>
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold text-slate-900">Reset Password</h1>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="password" class="text-sm font-medium text-slate-700"> New Password </label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="New Password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="8"
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="password2" class="text-sm font-medium text-slate-700">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
id="password2"
|
||||
type="password"
|
||||
name="password2"
|
||||
placeholder="Confirm New Password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="8"
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
||||
class="h-11 w-full rounded-lg border-slate-300 px-3 text-sm text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||
class="mt-1 flex h-11 w-full items-center justify-center rounded-lg bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</Window>
|
||||
|
||||
Reference in New Issue
Block a user