Compare commits

...

10 Commits
v0.0.2 ... main

Author SHA1 Message Date
Melon 5f6e2a3f01
Check if profileStore exists here
ci/woodpecker/push/build Pipeline was successful Details
2023-04-20 14:17:08 +01:00
Melon af13ad4a08
Use username instead of email for logins
ci/woodpecker/push/build Pipeline was successful Details
2023-02-18 14:24:46 +00:00
Melon b6b4b5c8f5
Mobile nav bar improvements
ci/woodpecker/push/build Pipeline was successful Details
2023-01-28 09:48:42 +00:00
Melon 8dd959bb03
Upgrade authorize page to have profile pictures
ci/woodpecker/push/build Pipeline was successful Details
2023-01-28 01:48:47 +00:00
Melon 1a1b4bb9dc
Format form styles
ci/woodpecker/push/build Pipeline was successful Details
2023-01-27 16:12:13 +00:00
Melon e3b6b8f38d
Remove external lucide svelte library and fix multiple calls to /user/@me on the profile page
ci/woodpecker/push/build Pipeline failed Details
2023-01-27 16:04:37 +00:00
Melon fc9aa54f8a
Move buttons and create separate form styles 2023-01-27 16:03:57 +00:00
Melon 66dafca64f
Remove unused imports
ci/woodpecker/push/build Pipeline was successful Details
2023-01-03 17:31:18 +00:00
Melon 4b9532ab23
Don't use getEnv function anymore
ci/woodpecker/push/build Pipeline was successful Details
2023-01-03 16:27:04 +00:00
Melon e5cab64698
Add promise all unique to run all promises
ci/woodpecker/push/build Pipeline was successful Details
2023-01-03 15:20:15 +00:00
33 changed files with 546 additions and 184 deletions

View File

@ -13,13 +13,13 @@ insert_final_newline = true
[*.css]
indent_size = 2
indent_style = space
trim_trailing_whitespace = true
trim_trailing_whitespace = false
# HTML
[*.{htm,html}]
indent_size = 2
indent_style = space
trim_trailing_whitespace = true
trim_trailing_whitespace = false
# GNU make
[Makefile]

View File

@ -8,16 +8,5 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
// overwrite these options
window.CONFIG = {
API_URL: undefined,
TITLE: undefined,
CSS_VAR: undefined,
LINK_TERMS: undefined,
LINK_PRIVACY: undefined,
};
</script>
</body>
</html>

View File

@ -20,7 +20,6 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.2",
"@tsconfig/svelte": "^3.0.0",
"lucide-svelte": "^0.102.0",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.8.0",
"sass": "^1.55.0",

View File

@ -1,19 +1,15 @@
<script lang="ts">
import {Router, Route, navigate, link} from "svelte-navigator";
import {getUser} from "./api/login";
import Menu from "./icons/Menu.svelte";
import HeaderDropdown from "./lib/HeaderDropdown.svelte";
import LazyComponent from "./lib/LazyComponent.svelte";
import {loginStore, profileStore, type LoginStore, type ProfileData} from "./stores/login";
import {getEnv} from "./utils/env";
let profile: ProfileData;
loginStore.subscribe((value: LoginStore) => {
getMe();
});
profileStore.subscribe((value: ProfileData) => (profile = value));
async function getMe() {
try {
let p = <ProfileData>await getUser("@me");
@ -22,12 +18,14 @@
profileStore.set(undefined);
}
}
let mobileNavOpen = false;
</script>
<svelte:head>
<title>{getEnv("TITLE")}</title>
<link rel="stylesheet" href="{getEnv('CSS_VAR')}.light.css" media="screen" />
<link rel="stylesheet" href="{getEnv('CSS_VAR')}.dark.css" media="screen and (prefers-color-scheme: dark)" />
<title>{import.meta.env.VITE_TITLE}</title>
<link rel="stylesheet" href="{import.meta.env.VITE_CSS_VAR}.light.css" media="screen" />
<link rel="stylesheet" href="{import.meta.env.VITE_CSS_VAR}.dark.css" media="screen and (prefers-color-scheme: dark)" />
</svelte:head>
<div id="app-router">
@ -35,12 +33,19 @@
<header>
<div class="central-header">
<a href="/" use:link>
<h1>{getEnv("TITLE")}</h1>
<h1>{import.meta.env.VITE_TITLE}</h1>
</a>
<nav>
{#if profile !== undefined}
<HeaderDropdown {profile} />
<div class="mobile-shade {mobileNavOpen ? 'mobile-open' : ''}" />
<button class="mobile-nav-toggle {mobileNavOpen ? 'mobile-active' : ''}" on:click={() => (mobileNavOpen = !mobileNavOpen)}>
<Menu size={32} />
</button>
<nav class={mobileNavOpen ? "mobile-open" : ""}>
{#if $profileStore !== undefined}
<div style="height:50px;">
<HeaderDropdown profile={$profileStore} />
</div>
{:else}
<a href="/register" use:link>Register</a>
<a href="/login" use:link>Login</a>
@ -107,6 +112,10 @@
max-width: min(100%, 1000px);
margin: auto;
> .mobile-nav-toggle {
display: none;
}
> nav {
height: 100%;
display: flex;
@ -135,5 +144,51 @@
> footer {
padding: 16px;
}
@media screen and (max-width: 600px) {
> header {
border-radius: 0;
> .central-header {
> .mobile-nav-toggle {
display: flex;
padding: 0;
width: 50px;
aspect-ratio: 1/1;
align-items: center;
justify-content: center;
&.mobile-active {
background-color: var(--primary-hover);
}
}
> nav {
position: fixed;
top: 50px;
left: 0;
right: 0;
height: auto;
z-index: 9998;
display: none;
&.mobile-open {
display: block;
background-color: var(--bg-panel-action);
}
}
> .mobile-shade.mobile-open {
position: fixed;
top: 50px;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
content: "";
z-index: 9997;
}
}
}
}
}
</style>

View File

@ -1,13 +1,10 @@
import {getEnv} from "~/utils/env";
import {loginStore} from "~/stores/login";
import {get} from "svelte/store";
export function URL() {
return getEnv("API_URL");
}
export const URL = import.meta.env.VITE_API_URL;
export async function sendApiRequest(path: string, opt: RequestInit) {
return fetch(URL() + path, opt);
return fetch(URL + path, opt);
}
export async function sendSessionRequest(path: string, opt: RequestInit) {

View File

@ -1,4 +1,5 @@
import {sendSessionRequest} from "./api";
export async function postRegister(data: object, token?: string) {
let headers = new Headers();
headers.set("Accept", "application/json");

View File

@ -70,3 +70,8 @@ button {
}
}
}
// useful utility styles
.flex-gap {
flex-grow: 1;
}

60
src/assets/form.scss Normal file
View File

@ -0,0 +1,60 @@
@mixin form-styles() {
h1 {
margin: 0;
padding: 32px 32px 0;
line-height: normal;
text-align: center;
}
h2 {
//margin-right: 300px;
> .optional {
font-size: 75%;
font-weight: 300;
}
}
section {
padding-bottom: 24px;
display: grid;
grid-template-columns: repeat(2, auto);
gap: 16px;
&.first-section {
padding-bottom: 8px;
}
label {
align-self: center;
}
input {
padding: 8px;
line-height: 24px;
width: 300px;
height: 16px;
border-radius: var(--small-curve);
border: 2px solid var(--primary-hover);
transition: border-color 100ms;
&:focus {
border: 2px solid var(--primary-text);
outline: none;
}
}
select {
padding: 8px;
width: 100%;
border-radius: var(--small-curve);
border: 2px solid var(--primary-hover);
transition: border-color 100ms;
&:focus {
border: 2px solid var(--primary-text);
outline: none;
}
}
}
}

View File

@ -24,6 +24,7 @@
</section>
</div>
<div class="email-code-action">
<div class="flex-gap" />
<button type="submit">Submit</button>
</div>
</form>
@ -80,6 +81,7 @@
background: var(--bg-panel-action);
padding: 24px;
border-radius: 0 0 var(--large-curve) var(--large-curve);
display: flex;
}
}
</style>

View File

@ -2,13 +2,13 @@
import {createEventDispatcher} from "svelte";
import {navigate} from "svelte-navigator";
let inputEmail: string = "";
let inputUsername: string = "";
let inputPassword: string = "";
const dispatch = createEventDispatcher();
function submitLogin() {
dispatch("submit", {email: inputEmail, password: inputPassword});
dispatch("submit", {username: inputUsername, password: inputPassword});
}
</script>
@ -17,8 +17,8 @@
<h1>Sign In</h1>
<section>
<label for="email">Email</label>
<input id="email" type="email" name="email" placeholder=" " autocomplete="username" required bind:value={inputEmail} />
<label for="username">Username</label>
<input id="username" type="text" name="username" placeholder=" " autocomplete="username" required bind:value={inputUsername} />
<label for="current-password">Password</label>
<input id="current-password" name="current-password" type="password" autocomplete="current-password" required bind:value={inputPassword} />
@ -27,10 +27,10 @@
<div class="login-action">
<section>
<button type="submit">Login</button>
<button on:click={() => navigate("/register")} class="grey-btn">Register</button>
<div class="flex-gap" />
<!-- <button on:click={() => navigate("/forgot-password")} class="grey-btn">Forgot My Password</button> -->
<button on:click={() => navigate("/register")} class="grey-btn">Register</button>
<button type="submit">Login</button>
</section>
</div>
</form>
@ -92,12 +92,14 @@
display: flex;
> .grey-btn {
color: var(--primary-hover);
margin-left: 8px;
background-color: #616161;
}
background-color: transparent;
> .flex-gap {
flex-grow: 1;
&:hover {
color: var(--primary-text);
background-color: #616161;
}
}
}
}

View File

@ -1,10 +1,16 @@
<script lang="ts">
import {CheckCircle, ExternalLink, Link2, Lock, Slash, XCircle} from "lucide-svelte";
import CheckCircle from "~/icons/CheckCircle.svelte";
import Link2 from "~/icons/Link2.svelte";
import Lock from "~/icons/Lock.svelte";
import XCircle from "~/icons/XCircle.svelte";
import {onMount} from "svelte";
import {navigate, useLocation} from "svelte-navigator";
import {getUser} from "~/api/login";
import {get} from "svelte/store";
import {getOAuthApp, getOAuthScopes, postAuthorize} from "~/api/oauth";
import LazyDelay from "~/lib/LazyDelay.svelte";
import {loginStore, profileStore} from "~/stores/login";
import {PromiseAllUnique} from "~/utils/promise-all-unique";
import MoreHorizontal from "~/icons/MoreHorizontal.svelte";
const fakeScope = [
"Eat cake",
@ -26,6 +32,7 @@
let app: {
app_name: string;
app_desc: string;
app_icon: string;
privacy?: string;
terms?: string;
};
@ -39,22 +46,23 @@
onMount(async () => {
let params = new URLSearchParams($location.search);
try {
await getUser("@me");
try {
app = await getOAuthApp(params.get("client_id"));
try {
scopes = await getOAuthScopes(params.get("scope"));
} catch (err) {
navigate("/oauth/invalid-scope?" + new URLSearchParams({redirect: params.get("redirect_uri")}));
}
} catch (_) {
navigate("/oauth/invalid-app?" + new URLSearchParams({redirect: params.get("redirect_uri")}));
}
} catch (_) {
if (get(loginStore) == null) {
let backParams = new URLSearchParams();
backParams.set("back", window.location.pathname + window.location.search);
navigate("/login?" + backParams.toString());
return;
}
try {
[app, scopes] = await PromiseAllUnique([getOAuthApp(params.get("client_id")), getOAuthScopes(params.get("scope"))]);
} catch (err) {
switch (err.index) {
case 0:
navigate("/oauth/invalid-app?" + new URLSearchParams({redirect: params.get("redirect_uri")}));
break;
case 1:
navigate("/oauth/invalid-scope?" + new URLSearchParams({redirect: params.get("redirect_uri")}));
break;
}
}
});
@ -87,7 +95,18 @@
{#if app}
<div class="oauth-widget">
<div class="oauth-content">
<div class="oauth-pictures">
<img src={app.app_icon} alt="" />
<span class="oauth-picture-separator">
<MoreHorizontal />
</span>
{#if $profileStore}
<img src={$profileStore.icon} alt="" />
{/if}
</div>
<h3 class="oauth-subtext">An external application</h3>
<h2 class="oauth-title">{app.app_name}</h2>
<h3 class="oauth-subtext">wants to access your Melon ID account</h3>
<div class="oauth-desc">{app.app_desc}</div>
<div class="oauth-scopes">
<ul>
@ -124,8 +143,8 @@
</div>
</div>
<div class="oauth-btns">
<button class="authorize-btn" on:click|preventDefault={runAuthorize}>Authorize</button>
<button class="cancel-btn secondary" on:click|preventDefault={runCancel}>Cancel</button>
<button class="authorize-btn" on:click|preventDefault={runAuthorize}>Authorize</button>
</div>
</div>
{:else}
@ -143,30 +162,62 @@
border-radius: var(--large-curve) var(--large-curve) 0 0;
padding: 0 16px;
> .oauth-title {
> .oauth-pictures {
padding: 2em;
display: flex;
justify-content: center;
> img {
width: 100px;
aspect-ratio: 1/1;
}
> .oauth-picture-separator {
margin: 30px;
display: flex;
align-items: center;
color: var(--primary-text);
}
}
> .oauth-subtext {
margin: 0;
padding: 32px 32px 16px 32px;
line-height: normal;
text-align: center;
font-size: 1em;
font-weight: 100;
color: #adadad;
}
> .oauth-title {
margin: 0;
padding: 8px 32px 8px 32px;
line-height: normal;
text-align: center;
color: #ffffff;
}
> .oauth-desc {
padding-bottom: 16px;
padding: 16px 0;
text-align: center;
border-bottom: 1px solid var(--bg-panel-action);
margin-top: 16px;
border-top: 1px solid var(--primary-text);
border-bottom: 1px solid var(--primary-text);
}
> .oauth-scopes {
margin: 0;
padding: 16px 0;
border-bottom: 1px solid var(--bg-panel-action);
border-bottom: 1px solid var(--primary-text);
> ul {
margin: 0;
padding-left: 24px;
display: flex;
flex-direction: column;
gap: 8px;
> li {
margin-top: 8px;
position: relative;
display: flex;

View File

@ -2,24 +2,26 @@
import {onMount} from "svelte";
import {getUser} from "~/api/login";
import LazyDelay from "~/lib/LazyDelay.svelte";
import type {ProfileData} from "~/stores/login";
import {profileStore, type ProfileData} from "~/stores/login";
export let id: "@me" | number;
let user: ProfileData;
let userIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==";
const defaultIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==";
onMount(async () => {
try {
user = await getUser(id);
userIcon = user.icon;
} catch (_) {}
if (id == "@me") user = $profileStore;
else {
try {
user = await getUser(id);
} catch (_) {}
}
});
</script>
<div class="profile-widget">
<div class="profile-content">
{#if user}
<img class="icon" src={userIcon} alt="Profile Icon" />
<img class="icon" src={user.icon || defaultIcon} alt="Profile Icon" />
<h1 class="displayName">{user.display_name}</h1>
<h2 class="username">{user.username}</h2>
<h3 class="email">{user.email}</h3>

View File

@ -1,14 +1,14 @@
<script lang="ts">
import {ExternalLink} from "lucide-svelte";
import ExternalLink from "~/icons/ExternalLink.svelte";
import {createEventDispatcher} from "svelte";
import {navigate} from "svelte-navigator";
import {getEnv} from "~/utils/env";
import PasswordConstraints from "~/lib/PasswordConstraints.svelte";
export let err: {message: string; log_id: string};
// Account
let inputEmail: string = "";
let inputTag: string = "";
let inputUsername: string = "";
let inputPassword: string = "";
let inputRepeat: string = "";
@ -23,7 +23,7 @@
function submitLogin() {
dispatch("submit", {
email: inputEmail,
username: inputTag,
username: inputUsername,
password: inputPassword,
repeatPassword: inputRepeat,
displayName: inputDisplayName,
@ -32,10 +32,6 @@
birthday: inputBirthday,
});
}
function conDone(v: boolean): string {
return v ? "follows-constraint" : "missing-constraint";
}
</script>
<form class="register-widget" method="post" on:submit|preventDefault={submitLogin}>
@ -46,10 +42,10 @@
<section class="first-section">
<label for="email">Email</label>
<input id="email" type="email" name="email" placeholder=" " autocomplete="username" required bind:value={inputEmail} />
<input id="email" type="email" name="email" placeholder=" " autocomplete="email" required bind:value={inputEmail} />
<label for="tag">Tag</label>
<input id="tag" type="text" name="tag" placeholder=" " autocomplete="off" required bind:value={inputTag} />
<label for="username">Username</label>
<input id="username" type="text" name="tag" placeholder=" " autocomplete="uesrname" required bind:value={inputUsername} />
<label for="new-password">Password</label>
<input
@ -75,15 +71,7 @@
</section>
<section>
<div id="password-constraints">
<ul>
<li class={conDone(inputPassword.length >= 8)}>Eight or more characters</li>
<li class={conDone(inputPassword.toLocaleLowerCase() != inputPassword)}>Uppercase characters</li>
<li class={conDone(inputPassword.toLocaleUpperCase() != inputPassword)}>Lowercase characters</li>
<li class={conDone(/\d/.test(inputPassword))}>Numeric digit</li>
<li class={conDone(/\W/.test(inputPassword))}>Special symbol</li>
</ul>
</div>
<PasswordConstraints password={inputPassword} />
</section>
<h2>Profile <span class="optional">(optional)</span></h2>
@ -112,9 +100,9 @@
<section>
<div>
You must agree to the
<a href={getEnv("LINK_TERMS")} target="_blank">Terms <ExternalLink size={16} /></a>
<a href={import.meta.env.VITE_LINK_TERMS} rel="noreferrer" target="_blank">Terms <ExternalLink size={16} /></a>
and
<a href={getEnv("LINK_PRIVACY")} target="_blank">Privacy <ExternalLink size={16} /></a>
<a href={import.meta.env.VITE_LINK_PRIVACY} rel="noreferrer" target="_blank">Privacy <ExternalLink size={16} /></a>
documents.
</div>
</section>
@ -128,15 +116,16 @@
</section>
{/if}
<section>
<button type="submit">Register</button>
<div class="flex-gap" />
<button on:click={() => navigate("/login")} class="grey-btn">Login</button>
<div class="flex-gap" />
<button type="submit">Register</button>
</section>
</div>
</form>
<style lang="scss">
@import "../../assets/panel.scss";
@import "../../assets/form.scss";
.register-widget {
@include panel;
@ -149,64 +138,7 @@
flex-direction: column;
align-items: center;
> h1 {
margin: 0;
padding: 32px 32px 0;
line-height: normal;
text-align: center;
}
> h2 {
//margin-right: 300px;
> .optional {
font-size: 75%;
font-weight: 300;
}
}
> section {
padding-bottom: 24px;
display: grid;
grid-template-columns: repeat(2, auto);
gap: 16px;
&.first-section {
padding-bottom: 8px;
}
> label {
align-self: center;
}
> input {
padding: 8px;
line-height: 24px;
width: 300px;
height: 16px;
border-radius: var(--small-curve);
border: 2px solid var(--primary-hover);
transition: border-color 100ms;
&:focus {
border: 2px solid var(--primary-text);
outline: none;
}
}
> select {
padding: 8px;
width: 100%;
border-radius: var(--small-curve);
border: 2px solid var(--primary-hover);
transition: border-color 100ms;
&:focus {
border: 2px solid var(--primary-text);
outline: none;
}
}
}
@include form-styles();
}
> .register-action {
@ -218,29 +150,19 @@
display: flex;
> .grey-btn {
color: var(--primary-hover);
margin-left: 0 8px;
background-color: #616161;
}
background-color: transparent;
> .flex-gap {
flex-grow: 1;
&:hover {
color: var(--primary-text);
background-color: #616161;
}
}
}
}
}
#password-constraints > ul {
margin: 0;
> li.follows-constraint {
color: darken(#bdd358, 5);
}
> li.missing-constraint {
color: darken(#e5625e, 5);
}
}
.error-message {
margin-bottom: 16px;
background-color: darken(#e5625e, 10);

View File

@ -1,12 +1,23 @@
<script lang="ts">
import LazyDelay from "~/lib/LazyDelay.svelte";
import PasswordConstraints from "~/lib/PasswordConstraints.svelte";
import {profileStore} from "~/stores/login";
let inputEmail: string = "";
let inputPassword: string = "";
let inputNewPassword: string = "";
let inputRepeatPassword: string = "";
function conDone(v: boolean): string {
return v ? "follows-constraint" : "missing-constraint";
}
</script>
<div class="settings-widget">
<div class="settings-content">
{#if $profileStore}
<div>Honestly I'm just too lazy to make this yet</div>
<h1>Settings</h1>
<section>Honestly I'm just too lazy to make this yet</section>
{:else}
<LazyDelay delayMs={500}>Loading...</LazyDelay>
{/if}
@ -15,6 +26,7 @@
<style lang="scss">
@import "../../assets/panel.scss";
@import "../../assets/form.scss";
.settings-widget {
@include panel;
@ -26,7 +38,9 @@
flex-direction: column;
padding: 12px 24px;
> .icon {
@include form-styles();
/*> .icon {
margin: 0 auto 12px auto;
width: 150px;
height: 150px;
@ -44,7 +58,7 @@
> .email {
margin: 0;
}
}*/
}
}
</style>

View File

@ -0,0 +1,18 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>

View File

@ -0,0 +1,17 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>

View File

@ -0,0 +1,17 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="18 15 12 9 6 15" />
</svg>

View File

@ -0,0 +1,19 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>

19
src/icons/Link2.svelte Normal file
View File

@ -0,0 +1,19 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 17H7A5 5 0 0 1 7 7h2" />
<path d="M15 7h2a5 5 0 1 1 0 10h-2" />
<line x1="8" y1="12" x2="16" y2="12" />
</svg>

18
src/icons/Lock.svelte Normal file
View File

@ -0,0 +1,18 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>

19
src/icons/LogOut.svelte Normal file
View File

@ -0,0 +1,19 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>

19
src/icons/Menu.svelte Normal file
View File

@ -0,0 +1,19 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>

View File

@ -0,0 +1,19 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>

20
src/icons/Settings.svelte Normal file
View File

@ -0,0 +1,20 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>

18
src/icons/User.svelte Normal file
View File

@ -0,0 +1,18 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>

19
src/icons/XCircle.svelte Normal file
View File

@ -0,0 +1,19 @@
<script lang="ts">
export let size = 24;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>

View File

@ -1,6 +1,10 @@
<script lang="ts">
import {loginStore, type ProfileData} from "~/stores/login";
import {User, Settings, LogOut, ChevronUp, ChevronDown} from "lucide-svelte";
import User from "~/icons/User.svelte";
import Settings from "~/icons/Settings.svelte";
import LogOut from "~/icons/LogOut.svelte";
import ChevronUp from "~/icons/ChevronUp.svelte";
import ChevronDown from "~/icons/ChevronDown.svelte";
import {link, navigate} from "svelte-navigator";
export let profile: ProfileData;
@ -55,7 +59,8 @@
</div>
{#if open}
<div class="dropdown-floating">
<div class="dropdown-body">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="dropdown-body" on:click={() => (open = false)}>
<a href="/profile" use:link>
<User />
<span>Profile</span>

View File

@ -0,0 +1,31 @@
<script lang="ts">
export let password: string;
function conDone(v: boolean): string {
return v ? "follows-constraint" : "missing-constraint";
}
</script>
<div id="password-constraints">
<ul>
<li class={conDone(password.length >= 8)}>Eight or more characters</li>
<li class={conDone(password.toLocaleLowerCase() != password)}>Uppercase characters</li>
<li class={conDone(password.toLocaleUpperCase() != password)}>Lowercase characters</li>
<li class={conDone(/\d/.test(password))}>Numeric digit</li>
<li class={conDone(/\W/.test(password))}>Special symbol</li>
</ul>
</div>
<style lang="scss">
#password-constraints > ul {
margin: 0;
> li.follows-constraint {
color: darken(#bdd358, 5);
}
> li.missing-constraint {
color: darken(#e5625e, 5);
}
}
</style>

View File

@ -26,6 +26,9 @@
if (params.has("back")) navigate(params.get("back"));
else navigate("/profile");
}
if (z.hasOwnProperty("log_id")) {
updatePage(postLogin({}, ""));
}
loading = false;
}
@ -51,5 +54,7 @@
<EmailCodeForm on:submit={submitLogin} />
{:else if step == "mfa"}
<!-- add mfa step -->
{:else}
<div>Something broke...</div>
{/if}
</Page>

View File

@ -3,11 +3,10 @@
import {navigate} from "svelte-navigator";
import Page from "~/lib/Page.svelte";
import Profile from "~/components/profile/Profile.svelte";
import {get} from "svelte/store";
import {profileStore} from "~/stores/login";
onMount(async () => {
if (get(profileStore) === undefined) navigate("/login?back=/profile");
if ($profileStore === undefined) navigate("/login?back=/profile");
});
</script>

View File

@ -1,4 +0,0 @@
export function getEnv(key: string) {
key = key.toUpperCase();
return window.CONFIG[key] ?? import.meta.env["VITE_" + key];
}

View File

@ -0,0 +1,10 @@
export async function PromiseAllUnique(promises: Promise<any>[]): Promise<any> {
return await Promise.all(
promises.map((promise, i) =>
promise.catch(err => {
err.index = i;
throw err;
}),
),
);
}

View File

@ -442,11 +442,6 @@ lower-case@^2.0.2:
dependencies:
tslib "^2.0.3"
lucide-svelte@^0.102.0:
version "0.102.0"
resolved "https://registry.yarnpkg.com/lucide-svelte/-/lucide-svelte-0.102.0.tgz#4a8bca665e6f01d21d60bb057996c35a6e878eb5"
integrity sha512-r8Nmz3XnRiesT3BxTaQvJnkbvutJMDv7HHADfDVZ1VLo3tWInfSBzWQya1V12dFfdBoz/bMwBX0abpccw77v4A==
magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"