UI consolidation
This commit is contained in:
49
src/components/PageHead.tsx
Normal file
49
src/components/PageHead.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Title, Meta, Link } from "@solidjs/meta";
|
||||||
|
|
||||||
|
export interface PageHeadProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
ogImage?: string;
|
||||||
|
ogTitle?: string;
|
||||||
|
ogDescription?: string;
|
||||||
|
canonical?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PageHead component for consistent page metadata across the application.
|
||||||
|
* Automatically appends " | Michael Freno" to the title.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <PageHead
|
||||||
|
* title="Blog"
|
||||||
|
* description="Technical blog posts about web development"
|
||||||
|
* ogImage="https://example.com/og-image.jpg"
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export default function PageHead(props: PageHeadProps) {
|
||||||
|
const fullTitle = () => `${props.title} | Michael Freno`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>{fullTitle()}</Title>
|
||||||
|
{props.description && (
|
||||||
|
<Meta name="description" content={props.description} />
|
||||||
|
)}
|
||||||
|
{props.canonical && <Link rel="canonical" href={props.canonical} />}
|
||||||
|
|
||||||
|
{/* Open Graph / Social Media Tags */}
|
||||||
|
{(props.ogTitle || props.title) && (
|
||||||
|
<Meta property="og:title" content={props.ogTitle || props.title} />
|
||||||
|
)}
|
||||||
|
{(props.ogDescription || props.description) && (
|
||||||
|
<Meta
|
||||||
|
property="og:description"
|
||||||
|
content={props.ogDescription || props.description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.ogImage && <Meta property="og:image" content={props.ogImage} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import TagMaker from "~/components/blog/TagMaker";
|
|||||||
import AddAttachmentSection from "~/components/blog/AddAttachmentSection";
|
import AddAttachmentSection from "~/components/blog/AddAttachmentSection";
|
||||||
import XCircle from "~/components/icons/XCircle";
|
import XCircle from "~/components/icons/XCircle";
|
||||||
import AddImageToS3 from "~/lib/s3upload";
|
import AddImageToS3 from "~/lib/s3upload";
|
||||||
|
import Input from "~/components/ui/Input";
|
||||||
|
|
||||||
interface PostFormProps {
|
interface PostFormProps {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
@@ -435,32 +436,26 @@ export default function PostForm(props: PostFormProps) {
|
|||||||
<form onSubmit={handleSubmit} class="w-full max-w-full px-4">
|
<form onSubmit={handleSubmit} class="w-full max-w-full px-4">
|
||||||
<div class="mx-auto w-full md:w-3/4 xl:w-1/2">
|
<div class="mx-auto w-full md:w-3/4 xl:w-1/2">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div class="input-group mx-4">
|
<Input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={title()}
|
||||||
value={title()}
|
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
name="title"
|
||||||
name="title"
|
label="Title"
|
||||||
placeholder=" "
|
containerClass="input-group mx-4"
|
||||||
class="underlinedInput w-full bg-transparent"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Title</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
<div class="input-group mx-4">
|
<Input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={subtitle()}
|
||||||
value={subtitle()}
|
onInput={(e) => setSubtitle(e.currentTarget.value)}
|
||||||
onInput={(e) => setSubtitle(e.currentTarget.value)}
|
name="subtitle"
|
||||||
name="subtitle"
|
label="Subtitle"
|
||||||
placeholder=" "
|
containerClass="input-group mx-4"
|
||||||
class="underlinedInput w-full bg-transparent"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Subtitle</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Banner */}
|
{/* Banner */}
|
||||||
<div class="pt-8 text-center text-xl">Banner</div>
|
<div class="pt-8 text-center text-xl">Banner</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { JSX, splitProps, Show } from "solid-js";
|
import { JSX, splitProps, Show } from "solid-js";
|
||||||
|
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||||
|
|
||||||
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||||
@@ -22,18 +23,28 @@ export default function Button(props: ButtonProps) {
|
|||||||
const size = () => local.size || "md";
|
const size = () => local.size || "md";
|
||||||
|
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
"flex justify-center items-center rounded font-semibold transition-all duration-300 ease-out disabled:opacity-50 disabled:cursor-not-allowed";
|
"flex justify-center items-center rounded transition-all duration-300 ease-out";
|
||||||
|
|
||||||
const variantClasses = () => {
|
const variantClasses = () => {
|
||||||
|
const isDisabledOrLoading = local.disabled || local.loading;
|
||||||
|
|
||||||
switch (variant()) {
|
switch (variant()) {
|
||||||
case "primary":
|
case "primary":
|
||||||
return "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700 text-white shadow-lg shadow-blue-300 dark:shadow-blue-700";
|
return isDisabledOrLoading
|
||||||
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
|
: "bg-blue hover:brightness-125 active:scale-90";
|
||||||
case "secondary":
|
case "secondary":
|
||||||
return "bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white";
|
return isDisabledOrLoading
|
||||||
|
? "bg-surface0 cursor-not-allowed brightness-75"
|
||||||
|
: "bg-surface0 hover:brightness-125 active:scale-90";
|
||||||
case "danger":
|
case "danger":
|
||||||
return "bg-red-500 hover:bg-red-600 active:scale-90 text-white shadow-lg shadow-red-300 dark:shadow-red-700";
|
return isDisabledOrLoading
|
||||||
|
? "bg-red cursor-not-allowed brightness-75"
|
||||||
|
: "bg-red hover:brightness-125 active:scale-90";
|
||||||
case "ghost":
|
case "ghost":
|
||||||
return "bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300";
|
return isDisabledOrLoading
|
||||||
|
? "cursor-not-allowed opacity-50"
|
||||||
|
: "hover:brightness-125 active:scale-90";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -61,27 +72,7 @@ export default function Button(props: ButtonProps) {
|
|||||||
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
|
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
|
||||||
>
|
>
|
||||||
<Show when={local.loading} fallback={local.children}>
|
<Show when={local.loading} fallback={local.children}>
|
||||||
<svg
|
<LoadingSpinner height={24} width={24} />
|
||||||
class="mr-2 h-5 w-5 animate-spin"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
class="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
class="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Loading...
|
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
|
|||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
|
containerClass?: string;
|
||||||
|
ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Input(props: InputProps) {
|
export default function Input(props: InputProps) {
|
||||||
@@ -11,13 +13,16 @@ export default function Input(props: InputProps) {
|
|||||||
"label",
|
"label",
|
||||||
"error",
|
"error",
|
||||||
"helperText",
|
"helperText",
|
||||||
"class"
|
"class",
|
||||||
|
"containerClass",
|
||||||
|
"ref"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="input-group">
|
<div class={local.containerClass || "input-group"}>
|
||||||
<input
|
<input
|
||||||
{...others}
|
{...others}
|
||||||
|
ref={local.ref}
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
class={`underlinedInput bg-transparent ${local.class || ""}`}
|
class={`underlinedInput bg-transparent ${local.class || ""}`}
|
||||||
aria-invalid={!!local.error}
|
aria-invalid={!!local.error}
|
||||||
|
|||||||
65
src/components/ui/PasswordInput.tsx
Normal file
65
src/components/ui/PasswordInput.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { JSX, splitProps, createSignal, Show } from "solid-js";
|
||||||
|
import Input, { InputProps } from "./Input";
|
||||||
|
import Eye from "~/components/icons/Eye";
|
||||||
|
import EyeSlash from "~/components/icons/EyeSlash";
|
||||||
|
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
|
||||||
|
|
||||||
|
export interface PasswordInputProps extends Omit<InputProps, "type"> {
|
||||||
|
showStrength?: boolean;
|
||||||
|
defaultVisible?: boolean;
|
||||||
|
passwordValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PasswordInput(props: PasswordInputProps) {
|
||||||
|
const [local, inputProps] = splitProps(props, [
|
||||||
|
"showStrength",
|
||||||
|
"defaultVisible",
|
||||||
|
"passwordValue",
|
||||||
|
"class",
|
||||||
|
"containerClass"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = createSignal(
|
||||||
|
local.defaultVisible || false
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class={local.containerClass || "input-group relative mx-4 mb-2"}>
|
||||||
|
<Input
|
||||||
|
{...inputProps}
|
||||||
|
type={showPassword() ? "text" : "password"}
|
||||||
|
class={`w-full pr-10 ${local.class || ""}`}
|
||||||
|
containerClass=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword())}
|
||||||
|
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
||||||
|
aria-label={showPassword() ? "Hide password" : "Show password"}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={showPassword()}
|
||||||
|
fallback={
|
||||||
|
<EyeSlash
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
strokeWidth={1}
|
||||||
|
class="stroke-text"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Eye height={24} width={24} strokeWidth={1} class="stroke-text" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{local.showStrength && local.passwordValue !== undefined && (
|
||||||
|
<div class="px-4 pt-1">
|
||||||
|
<PasswordStrengthMeter password={local.passwordValue} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -72,3 +72,32 @@ export function insertSoftHyphens(
|
|||||||
})
|
})
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function glitchText(
|
||||||
|
originalText: string,
|
||||||
|
setter: (text: string) => void,
|
||||||
|
glitchInterval: number = 300,
|
||||||
|
glitchLength: number = 80
|
||||||
|
) {
|
||||||
|
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Math.random() > 0.9) {
|
||||||
|
let glitched = "";
|
||||||
|
for (let i = 0; i < originalText.length; i++) {
|
||||||
|
if (Math.random() > 0.8) {
|
||||||
|
glitched +=
|
||||||
|
glitchChars[Math.floor(Math.random() * glitchChars.length)];
|
||||||
|
} else {
|
||||||
|
glitched += originalText[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setter(glitched);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setter(originalText);
|
||||||
|
}, glitchLength);
|
||||||
|
}
|
||||||
|
}, glitchInterval);
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { createSignal, Show, createEffect } from "solid-js";
|
|||||||
import { Title, Meta } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { useNavigate, redirect, query, createAsync } from "@solidjs/router";
|
import { useNavigate, redirect, query, createAsync } from "@solidjs/router";
|
||||||
import { getEvent } from "vinxi/http";
|
import { getEvent } from "vinxi/http";
|
||||||
import Eye from "~/components/icons/Eye";
|
|
||||||
import EyeSlash from "~/components/icons/EyeSlash";
|
|
||||||
import XCircle from "~/components/icons/XCircle";
|
import XCircle from "~/components/icons/XCircle";
|
||||||
import GoogleLogo from "~/components/icons/GoogleLogo";
|
import GoogleLogo from "~/components/icons/GoogleLogo";
|
||||||
import GitHub from "~/components/icons/GitHub";
|
import GitHub from "~/components/icons/GitHub";
|
||||||
@@ -14,6 +12,9 @@ import { validatePassword, isValidEmail } from "~/lib/validation";
|
|||||||
import { TerminalSplash } from "~/components/TerminalSplash";
|
import { TerminalSplash } from "~/components/TerminalSplash";
|
||||||
import { VALIDATION_CONFIG } from "~/config";
|
import { VALIDATION_CONFIG } from "~/config";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
import Input from "~/components/ui/Input";
|
||||||
|
import PasswordInput from "~/components/ui/PasswordInput";
|
||||||
|
import Button from "~/components/ui/Button";
|
||||||
|
|
||||||
import type { UserProfile } from "~/types/user";
|
import type { UserProfile } from "~/types/user";
|
||||||
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
|
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
|
||||||
@@ -87,10 +88,6 @@ export default function AccountPage() {
|
|||||||
const [passwordDeletionError, setPasswordDeletionError] = createSignal(false);
|
const [passwordDeletionError, setPasswordDeletionError] = createSignal(false);
|
||||||
const [newPassword, setNewPassword] = createSignal("");
|
const [newPassword, setNewPassword] = createSignal("");
|
||||||
|
|
||||||
const [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false);
|
|
||||||
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
|
||||||
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
|
||||||
|
|
||||||
const [showImageSuccess, setShowImageSuccess] = createSignal(false);
|
const [showImageSuccess, setShowImageSuccess] = createSignal(false);
|
||||||
const [showEmailSuccess, setShowEmailSuccess] = createSignal(false);
|
const [showEmailSuccess, setShowEmailSuccess] = createSignal(false);
|
||||||
const [showDisplayNameSuccess, setShowDisplayNameSuccess] =
|
const [showDisplayNameSuccess, setShowDisplayNameSuccess] =
|
||||||
@@ -662,25 +659,19 @@ export default function AccountPage() {
|
|||||||
JavaScript required to update email
|
JavaScript required to update email
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div class="input-group mx-4">
|
<Input
|
||||||
<input
|
ref={emailRef}
|
||||||
ref={emailRef}
|
type="email"
|
||||||
type="email"
|
required
|
||||||
required
|
disabled={
|
||||||
disabled={
|
emailButtonLoading() ||
|
||||||
emailButtonLoading() ||
|
(userProfile().email !== null &&
|
||||||
(userProfile().email !== null &&
|
!userProfile().emailVerified)
|
||||||
!userProfile().emailVerified)
|
}
|
||||||
}
|
title="Please enter a valid email address"
|
||||||
placeholder=" "
|
label={userProfile().email ? "Update Email" : "Add Email"}
|
||||||
title="Please enter a valid email address"
|
containerClass="input-group mx-4"
|
||||||
class="underlinedInput bg-transparent"
|
/>
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
{userProfile().email ? "Update Email" : "Add Email"}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
userProfile().provider !== "email" &&
|
userProfile().provider !== "email" &&
|
||||||
@@ -739,22 +730,15 @@ export default function AccountPage() {
|
|||||||
JavaScript required to update display name
|
JavaScript required to update display name
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div class="input-group mx-4">
|
<Input
|
||||||
<input
|
ref={displayNameRef}
|
||||||
ref={displayNameRef}
|
type="text"
|
||||||
type="text"
|
required
|
||||||
required
|
disabled={displayNameButtonLoading()}
|
||||||
disabled={displayNameButtonLoading()}
|
title="Please enter your display name"
|
||||||
placeholder=" "
|
label={`Set ${userProfile().displayName ? "New " : ""}Display Name`}
|
||||||
title="Please enter your display name"
|
containerClass="input-group mx-4"
|
||||||
class="underlinedInput bg-transparent"
|
/>
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
Set {userProfile().displayName ? "New " : ""}Display
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -806,136 +790,40 @@ export default function AccountPage() {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={userProfile().hasPassword}>
|
<Show when={userProfile().hasPassword}>
|
||||||
<div class="input-group relative mx-4 mb-6">
|
<PasswordInput
|
||||||
<input
|
ref={oldPasswordRef}
|
||||||
ref={oldPasswordRef}
|
required
|
||||||
type={showOldPasswordInput() ? "text" : "password"}
|
minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
|
||||||
required
|
disabled={passwordChangeLoading()}
|
||||||
minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
|
title="Password must be at least 8 characters"
|
||||||
disabled={passwordChangeLoading()}
|
label="Old Password"
|
||||||
placeholder=" "
|
containerClass="input-group relative mx-4 mb-6"
|
||||||
title="Password must be at least 8 characters"
|
/>
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Old Password</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setShowOldPasswordInput(!showOldPasswordInput())
|
|
||||||
}
|
|
||||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showOldPasswordInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="input-group relative mx-4 mb-2">
|
<PasswordInput
|
||||||
<input
|
ref={newPasswordRef}
|
||||||
ref={newPasswordRef}
|
required
|
||||||
type={showPasswordInput() ? "text" : "password"}
|
minlength="8"
|
||||||
required
|
onInput={handleNewPasswordChange}
|
||||||
minlength="8"
|
onBlur={handlePasswordBlur}
|
||||||
onInput={handleNewPasswordChange}
|
disabled={passwordChangeLoading()}
|
||||||
onBlur={handlePasswordBlur}
|
title="Password must be at least 8 characters"
|
||||||
disabled={passwordChangeLoading()}
|
label="New Password"
|
||||||
placeholder=" "
|
showStrength
|
||||||
title="Password must be at least 8 characters"
|
passwordValue={newPassword()}
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
containerClass="input-group relative mx-4 mb-2"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
<PasswordInput
|
||||||
<label class="underlinedInputLabel">New Password</label>
|
ref={newPasswordConfRef}
|
||||||
<div class="pt-1">
|
required
|
||||||
<PasswordStrengthMeter password={newPassword()} />
|
minlength="8"
|
||||||
</div>
|
onInput={handlePasswordConfChange}
|
||||||
<button
|
disabled={passwordChangeLoading()}
|
||||||
type="button"
|
title="Password must be at least 8 characters"
|
||||||
onClick={() =>
|
label="New Password Confirmation"
|
||||||
setShowPasswordInput(!showPasswordInput())
|
containerClass="input-group relative mx-4 mb-2"
|
||||||
}
|
/>
|
||||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="input-group relative mx-4 mb-2">
|
|
||||||
<input
|
|
||||||
ref={newPasswordConfRef}
|
|
||||||
type={showPasswordConfInput() ? "text" : "password"}
|
|
||||||
required
|
|
||||||
minlength="8"
|
|
||||||
onInput={handlePasswordConfChange}
|
|
||||||
disabled={passwordChangeLoading()}
|
|
||||||
placeholder=" "
|
|
||||||
title="Password must be at least 8 characters"
|
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
New Password Confirmation
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setShowPasswordConfInput(!showPasswordConfInput())
|
|
||||||
}
|
|
||||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordConfInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
@@ -1039,22 +927,15 @@ export default function AccountPage() {
|
|||||||
>
|
>
|
||||||
<form onSubmit={deleteAccountTrigger}>
|
<form onSubmit={deleteAccountTrigger}>
|
||||||
<div class="flex w-full justify-center">
|
<div class="flex w-full justify-center">
|
||||||
<div class="input-group delete mx-4">
|
<PasswordInput
|
||||||
<input
|
ref={deleteAccountPasswordRef}
|
||||||
ref={deleteAccountPasswordRef}
|
required
|
||||||
type="password"
|
minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
|
||||||
required
|
disabled={deleteAccountButtonLoading()}
|
||||||
minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
|
title="Enter your password to confirm account deletion"
|
||||||
disabled={deleteAccountButtonLoading()}
|
label="Enter Password"
|
||||||
placeholder=" "
|
containerClass="input-group delete mx-4"
|
||||||
title="Enter your password to confirm account deletion"
|
/>
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
Enter Password
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -852,3 +852,6 @@ pre {
|
|||||||
scrollbar-color: var(--color-text) transparent;
|
scrollbar-color: var(--color-text) transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
max-height: 50vh !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getClientCookie, setClientCookie } from "~/lib/cookies.client";
|
|||||||
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||||
import LoadingSpinner from "~/components/LoadingSpinner";
|
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||||
import RevealDropDown from "~/components/RevealDropDown";
|
import RevealDropDown from "~/components/RevealDropDown";
|
||||||
|
import Input from "~/components/ui/Input";
|
||||||
import type { UserProfile } from "~/types/user";
|
import type { UserProfile } from "~/types/user";
|
||||||
import { getCookie, setCookie } from "vinxi/http";
|
import { getCookie, setCookie } from "vinxi/http";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -379,32 +380,26 @@ export default function ContactPage() {
|
|||||||
>
|
>
|
||||||
<div class="flex w-full flex-col justify-evenly">
|
<div class="flex w-full flex-col justify-evenly">
|
||||||
<div class="mx-auto w-full justify-evenly md:flex md:flex-row">
|
<div class="mx-auto w-full justify-evenly md:flex md:flex-row">
|
||||||
<div class="input-group md:mx-4">
|
<Input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
required
|
||||||
required
|
name="name"
|
||||||
name="name"
|
value={user()?.displayName ?? ""}
|
||||||
value={user()?.displayName ?? ""}
|
title="Please enter your name"
|
||||||
placeholder=" "
|
label="Name"
|
||||||
title="Please enter your name"
|
containerClass="input-group md:mx-4"
|
||||||
class="underlinedInput w-full bg-transparent"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
<Input
|
||||||
<label class="underlinedInputLabel">Name</label>
|
type="email"
|
||||||
</div>
|
required
|
||||||
<div class="input-group md:mx-4">
|
name="email"
|
||||||
<input
|
value={user()?.email ?? ""}
|
||||||
type="email"
|
title="Please enter a valid email address"
|
||||||
required
|
label="Email"
|
||||||
name="email"
|
containerClass="input-group md:mx-4"
|
||||||
value={user()?.email ?? ""}
|
class="w-full"
|
||||||
placeholder=" "
|
/>
|
||||||
title="Please enter a valid email address"
|
|
||||||
class="underlinedInput w-full bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Email</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto w-full pt-6 md:pt-12">
|
<div class="mx-auto w-full pt-6 md:pt-12">
|
||||||
<div class="textarea-group">
|
<div class="textarea-group">
|
||||||
|
|||||||
@@ -2,9 +2,29 @@ import { Title, Meta } from "@solidjs/meta";
|
|||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
import { createSignal, onMount, onCleanup } from "solid-js";
|
import { createSignal, onMount, onCleanup } from "solid-js";
|
||||||
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
||||||
|
import { glitchText } from "~/lib/client-utils";
|
||||||
|
|
||||||
|
const DownloadButton = ({
|
||||||
|
onClick,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
children: Element | string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
class="bg-green hover:bg-green/90 cursor-pointer rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function DownloadsPage() {
|
export default function DownloadsPage() {
|
||||||
const [glitchText, setGlitchText] = createSignal("$ downloads");
|
const [LaLText, setLaLText] = createSignal("Life and Lineage");
|
||||||
|
const [SwAText, setSwAText] = createSignal("Shapes with Abigail!");
|
||||||
|
const [corkText, setCorkText] = createSignal("Cork");
|
||||||
|
|
||||||
const download = (assetName: string) => {
|
const download = (assetName: string) => {
|
||||||
fetch(`/api/downloads/public/${assetName}`)
|
fetch(`/api/downloads/public/${assetName}`)
|
||||||
@@ -17,30 +37,14 @@ export default function DownloadsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const originalText = "$ downloads";
|
const lalInterval = glitchText(LaLText(), setLaLText);
|
||||||
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
|
const swaInterval = glitchText(SwAText(), setSwAText);
|
||||||
|
const corkInterval = glitchText(corkText(), setCorkText);
|
||||||
const glitchInterval = setInterval(() => {
|
|
||||||
if (Math.random() > 0.9) {
|
|
||||||
let glitched = "";
|
|
||||||
for (let i = 0; i < originalText.length; i++) {
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
glitched +=
|
|
||||||
glitchChars[Math.floor(Math.random() * glitchChars.length)];
|
|
||||||
} else {
|
|
||||||
glitched += originalText[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setGlitchText(glitched);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setGlitchText(originalText);
|
|
||||||
}, 80);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
clearInterval(glitchInterval);
|
clearInterval(lalInterval);
|
||||||
|
clearInterval(swaInterval);
|
||||||
|
clearInterval(corkInterval);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,18 +69,11 @@ export default function DownloadsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<div class="text-text mb-12 font-mono text-3xl tracking-wider">
|
|
||||||
<span class="text-green">freno@downloads</span>
|
|
||||||
<span class="text-subtext1">:</span>
|
|
||||||
<span class="text-blue">~</span>
|
|
||||||
<span class="text-subtext1 ml-2">{glitchText()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mx-auto max-w-5xl space-y-16">
|
<div class="mx-auto max-w-5xl space-y-16">
|
||||||
{/* Life and Lineage */}
|
{/* Life and Lineage */}
|
||||||
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
||||||
<h2 class="text-text mb-6 font-mono text-2xl">
|
<h2 class="text-text mb-6 font-mono text-2xl">
|
||||||
<span class="text-yellow">{">"}</span> Life and Lineage
|
<span class="text-yellow">{">"}</span> {LaLText()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-8 md:flex-row md:justify-around">
|
<div class="flex flex-col gap-8 md:flex-row md:justify-around">
|
||||||
@@ -84,12 +81,9 @@ export default function DownloadsPage() {
|
|||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: android
|
platform: android
|
||||||
</span>
|
</span>
|
||||||
<button
|
<DownloadButton onClick={() => download("lineage")}>
|
||||||
onClick={() => download("lineage")}
|
|
||||||
class="bg-green hover:bg-green/90 rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
|
|
||||||
>
|
|
||||||
download.apk
|
download.apk
|
||||||
</button>
|
</DownloadButton>
|
||||||
<span class="text-subtext1 max-w-xs text-center text-xs italic">
|
<span class="text-subtext1 max-w-xs text-center text-xs italic">
|
||||||
# android build not optimized
|
# android build not optimized
|
||||||
</span>
|
</span>
|
||||||
@@ -112,7 +106,7 @@ export default function DownloadsPage() {
|
|||||||
{/* Shapes with Abigail */}
|
{/* Shapes with Abigail */}
|
||||||
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
||||||
<h2 class="text-text mb-6 font-mono text-2xl">
|
<h2 class="text-text mb-6 font-mono text-2xl">
|
||||||
<span class="text-yellow">{">"}</span> Shapes with Abigail!
|
<span class="text-yellow">{">"}</span> {SwAText()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-8 md:flex-row md:justify-around">
|
<div class="flex flex-col gap-8 md:flex-row md:justify-around">
|
||||||
@@ -120,12 +114,11 @@ export default function DownloadsPage() {
|
|||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: android
|
platform: android
|
||||||
</span>
|
</span>
|
||||||
<button
|
<DownloadButton
|
||||||
onClick={() => download("shapes-with-abigail")}
|
onClick={() => download("shapes-with-abigail")}
|
||||||
class="bg-green hover:bg-green/90 rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
|
|
||||||
>
|
>
|
||||||
download.apk
|
download.apk
|
||||||
</button>
|
</DownloadButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-3">
|
<div class="flex flex-col items-center gap-3">
|
||||||
@@ -145,19 +138,16 @@ export default function DownloadsPage() {
|
|||||||
{/* Cork */}
|
{/* Cork */}
|
||||||
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
|
||||||
<h2 class="text-text mb-6 font-mono text-2xl">
|
<h2 class="text-text mb-6 font-mono text-2xl">
|
||||||
<span class="text-yellow">{">"}</span> Cork
|
<span class="text-yellow">{">"}</span> {corkText()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-3">
|
<div class="flex flex-col items-center gap-3">
|
||||||
<span class="text-subtext0 font-mono text-sm">
|
<span class="text-subtext0 font-mono text-sm">
|
||||||
platform: macOS (13+)
|
platform: macOS (13+)
|
||||||
</span>
|
</span>
|
||||||
<button
|
<DownloadButton onClick={() => download("cork")}>
|
||||||
onClick={() => download("cork")}
|
|
||||||
class="bg-green hover:bg-green/90 rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
|
|
||||||
>
|
|
||||||
download.zip
|
download.zip
|
||||||
</button>
|
</DownloadButton>
|
||||||
<span class="text-subtext1 text-xs">
|
<span class="text-subtext1 text-xs">
|
||||||
# unzip → drag to /Applications
|
# unzip → drag to /Applications
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import { Title, Meta } from "@solidjs/meta";
|
|||||||
import { getEvent } from "vinxi/http";
|
import { getEvent } from "vinxi/http";
|
||||||
import GoogleLogo from "~/components/icons/GoogleLogo";
|
import GoogleLogo from "~/components/icons/GoogleLogo";
|
||||||
import GitHub from "~/components/icons/GitHub";
|
import GitHub from "~/components/icons/GitHub";
|
||||||
import Eye from "~/components/icons/Eye";
|
|
||||||
import EyeSlash from "~/components/icons/EyeSlash";
|
|
||||||
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||||
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
|
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
|
||||||
import { isValidEmail, validatePassword } from "~/lib/validation";
|
import { isValidEmail, validatePassword } from "~/lib/validation";
|
||||||
import { getClientCookie } from "~/lib/cookies.client";
|
import { getClientCookie } from "~/lib/cookies.client";
|
||||||
import { env } from "~/env/client";
|
import { env } from "~/env/client";
|
||||||
import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config";
|
import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config";
|
||||||
|
import Input from "~/components/ui/Input";
|
||||||
|
import PasswordInput from "~/components/ui/PasswordInput";
|
||||||
|
|
||||||
const checkAuth = query(async () => {
|
const checkAuth = query(async () => {
|
||||||
"use server";
|
"use server";
|
||||||
@@ -49,8 +49,6 @@ export default function LoginPage() {
|
|||||||
const [emailSent, setEmailSent] = createSignal(false);
|
const [emailSent, setEmailSent] = createSignal(false);
|
||||||
const [showPasswordError, setShowPasswordError] = createSignal(false);
|
const [showPasswordError, setShowPasswordError] = createSignal(false);
|
||||||
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
|
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
|
||||||
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
|
||||||
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
|
||||||
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
||||||
const [password, setPassword] = createSignal("");
|
const [password, setPassword] = createSignal("");
|
||||||
const [passwordConf, setPasswordConf] = createSignal("");
|
const [passwordConf, setPasswordConf] = createSignal("");
|
||||||
@@ -402,116 +400,47 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<form onSubmit={formHandler} class="flex flex-col px-2 py-4">
|
<form onSubmit={formHandler} class="flex flex-col px-2 py-4">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="input-group mx-4">
|
<Input
|
||||||
<input
|
type="email"
|
||||||
type="email"
|
required
|
||||||
required
|
ref={emailRef}
|
||||||
ref={emailRef}
|
title="Please enter a valid email address"
|
||||||
placeholder=" "
|
label="Email"
|
||||||
title="Please enter a valid email address"
|
containerClass="input-group mx-4"
|
||||||
class="underlinedInput bg-transparent"
|
/>
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Email</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={usePassword() || register()}>
|
<Show when={usePassword() || register()}>
|
||||||
<div class="-mt-4 flex justify-center">
|
<div class="-mt-4 flex justify-center">
|
||||||
<div class="input-group mx-4 flex">
|
<PasswordInput
|
||||||
<input
|
required
|
||||||
type={showPasswordInput() ? "text" : "password"}
|
minLength={8}
|
||||||
required
|
ref={passwordRef}
|
||||||
minLength={8}
|
onInput={register() ? handlePasswordChange : undefined}
|
||||||
ref={passwordRef}
|
title="Password must be at least 8 characters"
|
||||||
onInput={register() ? handlePasswordChange : undefined}
|
label="Password"
|
||||||
placeholder=" "
|
containerClass="input-group mx-4 flex"
|
||||||
title="Password must be at least 8 characters"
|
showStrength={register()}
|
||||||
class="underlinedInput bg-transparent"
|
passwordValue={register() ? password() : undefined}
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Password</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordInput(!showPasswordInput());
|
|
||||||
passwordRef?.focus();
|
|
||||||
}}
|
|
||||||
class="absolute mt-14 ml-60"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={register()}>
|
|
||||||
<div class="mx-auto flex justify-center px-4 py-2">
|
|
||||||
<PasswordStrengthMeter password={password()} />
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={register()}>
|
<Show when={register()}>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="input-group mx-4">
|
<PasswordInput
|
||||||
<input
|
required
|
||||||
type={showPasswordConfInput() ? "text" : "password"}
|
minLength={8}
|
||||||
required
|
ref={passwordConfRef}
|
||||||
minLength={8}
|
onInput={handlePasswordConfChange}
|
||||||
ref={passwordConfRef}
|
title="Password must be at least 8 characters and match the password above"
|
||||||
onInput={handlePasswordConfChange}
|
label="Confirm Password"
|
||||||
placeholder=" "
|
containerClass="input-group mx-4"
|
||||||
title="Password must be at least 8 characters and match the password above"
|
/>
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Confirm Password</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordConfInput(!showPasswordConfInput());
|
|
||||||
passwordConfRef?.focus();
|
|
||||||
}}
|
|
||||||
class="absolute mt-14 ml-60"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordConfInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={register()}>
|
||||||
<div
|
<div
|
||||||
class={`${
|
class={`${
|
||||||
!passwordsMatch() &&
|
!passwordsMatch() &&
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { createSignal, createEffect, Show } from "solid-js";
|
|||||||
import { A, useNavigate, useSearchParams } from "@solidjs/router";
|
import { A, useNavigate, useSearchParams } from "@solidjs/router";
|
||||||
import { Title, Meta } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||||
import Eye from "~/components/icons/Eye";
|
|
||||||
import EyeSlash from "~/components/icons/EyeSlash";
|
|
||||||
import { validatePassword } from "~/lib/validation";
|
import { validatePassword } from "~/lib/validation";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config";
|
import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config";
|
||||||
|
import PasswordInput from "~/components/ui/PasswordInput";
|
||||||
|
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
|
||||||
|
|
||||||
export default function PasswordResetPage() {
|
export default function PasswordResetPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -22,8 +22,7 @@ export default function PasswordResetPage() {
|
|||||||
const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false);
|
const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false);
|
||||||
const [countDown, setCountDown] = createSignal(false);
|
const [countDown, setCountDown] = createSignal(false);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
const [newPassword, setNewPassword] = createSignal("");
|
||||||
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
|
||||||
|
|
||||||
let newPasswordRef: HTMLInputElement | undefined;
|
let newPasswordRef: HTMLInputElement | undefined;
|
||||||
let newPasswordConfRef: HTMLInputElement | undefined;
|
let newPasswordConfRef: HTMLInputElement | undefined;
|
||||||
@@ -121,6 +120,7 @@ export default function PasswordResetPage() {
|
|||||||
|
|
||||||
const handleNewPasswordChange = (e: Event) => {
|
const handleNewPasswordChange = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
|
setNewPassword(target.value);
|
||||||
checkPasswordLength(target.value);
|
checkPasswordLength(target.value);
|
||||||
if (newPasswordConfRef) {
|
if (newPasswordConfRef) {
|
||||||
checkForMatch(target.value, newPasswordConfRef.value);
|
checkForMatch(target.value, newPasswordConfRef.value);
|
||||||
@@ -169,49 +169,19 @@ export default function PasswordResetPage() {
|
|||||||
>
|
>
|
||||||
<div class="flex w-full max-w-md flex-col justify-center px-4">
|
<div class="flex w-full max-w-md flex-col justify-center px-4">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="input-group mx-4 flex">
|
<PasswordInput
|
||||||
<input
|
ref={newPasswordRef}
|
||||||
ref={newPasswordRef}
|
name="newPassword"
|
||||||
name="newPassword"
|
required
|
||||||
type={showPasswordInput() ? "text" : "password"}
|
autofocus
|
||||||
required
|
onInput={handleNewPasswordChange}
|
||||||
autofocus
|
onBlur={handlePasswordBlur}
|
||||||
onInput={handleNewPasswordChange}
|
disabled={passwordChangeLoading()}
|
||||||
onBlur={handlePasswordBlur}
|
label="New Password"
|
||||||
disabled={passwordChangeLoading()}
|
containerClass="input-group mx-4 flex"
|
||||||
placeholder=" "
|
showStrength
|
||||||
class="underlinedInput bg-transparent"
|
passwordValue={newPassword()}
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">New Password</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordInput(!showPasswordInput());
|
|
||||||
newPasswordRef?.focus();
|
|
||||||
}}
|
|
||||||
class="absolute mt-14 ml-60"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -224,49 +194,15 @@ export default function PasswordResetPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="-mt-4 flex justify-center">
|
<div class="-mt-4 flex justify-center">
|
||||||
<div class="input-group mx-4 flex">
|
<PasswordInput
|
||||||
<input
|
ref={newPasswordConfRef}
|
||||||
ref={newPasswordConfRef}
|
name="newPasswordConf"
|
||||||
name="newPasswordConf"
|
onInput={handlePasswordConfChange}
|
||||||
onInput={handlePasswordConfChange}
|
required
|
||||||
type={showPasswordConfInput() ? "text" : "password"}
|
disabled={passwordChangeLoading()}
|
||||||
required
|
label="Password Confirmation"
|
||||||
disabled={passwordChangeLoading()}
|
containerClass="input-group mx-4 flex"
|
||||||
placeholder=" "
|
/>
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
Password Confirmation
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordConfInput(!showPasswordConfInput());
|
|
||||||
newPasswordConfRef?.focus();
|
|
||||||
}}
|
|
||||||
class="absolute mt-14 ml-60"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordConfInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-text"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
|||||||
import { isValidEmail } from "~/lib/validation";
|
import { isValidEmail } from "~/lib/validation";
|
||||||
import { getClientCookie } from "~/lib/cookies.client";
|
import { getClientCookie } from "~/lib/cookies.client";
|
||||||
import { COUNTDOWN_CONFIG } from "~/config";
|
import { COUNTDOWN_CONFIG } from "~/config";
|
||||||
|
import Input from "~/components/ui/Input";
|
||||||
|
|
||||||
export default function RequestPasswordResetPage() {
|
export default function RequestPasswordResetPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -136,20 +137,17 @@ export default function RequestPasswordResetPage() {
|
|||||||
class="mt-4 flex w-full justify-center"
|
class="mt-4 flex w-full justify-center"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<div class="input-group mx-4">
|
<Input
|
||||||
<input
|
ref={emailRef}
|
||||||
ref={emailRef}
|
name="email"
|
||||||
name="email"
|
type="email"
|
||||||
type="email"
|
required
|
||||||
required
|
disabled={loading()}
|
||||||
disabled={loading()}
|
title="Please enter a valid email address"
|
||||||
placeholder=" "
|
label="Enter Email"
|
||||||
title="Please enter a valid email address"
|
containerClass="input-group mx-4"
|
||||||
class="underlinedInput w-full bg-transparent"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Enter Email</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={countDown() > 0}
|
when={countDown() > 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user