Compare commits

..

16 Commits

Author SHA1 Message Date
Juan Di Toro 1732c18059 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into riobuenoDevelops/my-account 2024-03-14 16:43:28 +01:00
Juan Di Toro a1e73b8ed8 fix: formatting 2024-03-14 16:41:48 +01:00
Derrick Hammer 5f97e8c7d8
feat: implement identity api 2024-03-14 07:10:17 -04:00
Derrick Hammer 0b5e066aab
dep: update portal-sdk 2024-03-14 07:10:00 -04:00
Derrick Hammer 48bb9d1121
refactor: handle redirects from ?to after login, or when already logged in 2024-03-13 19:23:29 -04:00
Derrick Hammer 4c972d289d
chore: update package-lock.json 2024-03-13 19:08:43 -04:00
Derrick Hammer 70a4139b93
feat: add cookie support for auth 2024-03-13 19:08:14 -04:00
Derrick Hammer 0d7215ff66
feat: implement register 2024-03-13 19:07:46 -04:00
Derrick Hammer 7b645666f6
dep: update sdk, add cookie lib 2024-03-13 18:49:19 -04:00
Juan Di Toro d07dbff31f feat: add first and last name to register form 2024-03-13 19:06:43 +01:00
Derrick Hammer 99ac9533f0
fix: update submit button name 2024-03-13 13:35:42 -04:00
Derrick Hammer 3f77d16f0b
style: prettier 2024-03-13 13:33:03 -04:00
Derrick Hammer 273b41d243
refactor: change register name from "sign up" to "register" 2024-03-13 13:32:07 -04:00
Derrick Hammer 4259f38566
chore: remove un-used import 2024-03-13 13:19:41 -04:00
Derrick Hammer c06ca5afe9
chore: update package-lock.json 2024-03-13 13:18:51 -04:00
Derrick Hammer 51618a2fda
feat: setup auth provider and refine with login, check and logout methods 2024-03-13 13:18:18 -04:00
9 changed files with 20489 additions and 322 deletions

View File

@ -1,37 +1,166 @@
import { AuthProvider } from "@refinedev/core" import type {AuthProvider} from "@refinedev/core"
import type {
AuthActionResponse,
CheckResponse,
OnErrorResponse
// @ts-ignore
} from "@refinedev/core/dist/interfaces/bindings/auth"
export const authProvider: AuthProvider = { import type {
login: async (params: any) => { AuthActionResponse,
return { success: true } satisfies AuthActionResponse CheckResponse,
}, IdentityResponse,
logout: async (params: any) => { OnErrorResponse
return { success: true } satisfies AuthActionResponse // @ts-ignore
}, } from "@refinedev/core/dist/interfaces/bindings/auth"
check: async (params?: any) => { import {Sdk} from "@lumeweb/portal-sdk";
return { authenticated: true } satisfies CheckResponse import Cookies from 'universal-cookie';
}, import type {AccountInfoResponse} from "@lumeweb/portal-sdk";
onError: async (error: any) => {
return { logout: true } satisfies OnErrorResponse export type AuthFormRequest = {
}, email: string;
register: async (params: any) => { password: string;
return { success: true } satisfies AuthActionResponse rememberMe: boolean;
}, redirectTo?: string;
forgotPassword: async (params: any) => { }
return { success: true } satisfies AuthActionResponse
}, export type RegisterFormRequest = {
updatePassword: async (params: any) => { email: string;
return { success: true } satisfies AuthActionResponse password: string;
}, firstName: string;
getPermissions: async (params: any) => { lastName: string;
return { success: true } satisfies AuthActionResponse }
},
getIdentity: async (params: any) => { export type Identity = {
return { id: "1", fullName: "John Doe", avatar: "https://via.placeholder.com/150" } id: string;
} firstName: string;
lastName: string;
email: string;
}
export class PortalAuthProvider implements RequiredAuthProvider {
private sdk: Sdk;
constructor(apiUrl: string) {
this.sdk = Sdk.create(apiUrl);
const methods: Array<keyof AuthProvider> = [
'login',
'logout',
'check',
'onError',
'register',
'forgotPassword',
'updatePassword',
'getPermissions',
'getIdentity',
];
methods.forEach((method) => {
this[method] = this[method]?.bind(this) as any;
});
}
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const cookies = new Cookies();
const ret = await this.sdk.account().login({
email: params.email,
password: params.password,
})
let redirectTo: string | undefined;
if (ret) {
cookies.set('jwt', this.sdk.account().jwtToken, {path: '/'});
redirectTo = params.redirectTo;
if (!redirectTo) {
redirectTo = ret ? "/dashboard" : "/login";
}
}
return {
success: ret,
redirectTo,
};
}
async logout(params: any): Promise<AuthActionResponse> {
let ret = await this.sdk.account().logout();
if (ret) {
const cookies = new Cookies();
cookies.remove('jwt');
}
return {success: ret, redirectTo: "/login"};
}
async check(params?: any): Promise<CheckResponse> {
const cookies = new Cookies();
const jwtCookie = cookies.get('jwt');
if (jwtCookie) {
this.sdk.setAuthToken(jwtCookie);
}
const ret = await this.sdk.account().ping();
if (!ret) {
cookies.remove('jwt');
}
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
}
async onError(error: any): Promise<OnErrorResponse> {
return {logout: true};
}
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
const ret = await this.sdk.account().register({
email: params.email,
password: params.password,
first_name: params.firstName,
last_name: params.lastName,
});
return {success: ret, redirectTo: ret ? "/dashboard" : undefined};
}
async forgotPassword(params: any): Promise<AuthActionResponse> {
return {success: true};
}
async updatePassword(params: any): Promise<AuthActionResponse> {
return {success: true};
}
async getPermissions(params?: Record<string, any>): Promise<AuthActionResponse> {
return {success: true};
}
async getIdentity(params?: Identity): Promise<IdentityResponse> {
const ret = await this.sdk.account().info();
if (!ret) {
return {identity: null};
}
const acct = ret as AccountInfoResponse;
return {
id: acct.id,
firstName: acct.first_name,
lastName: acct.last_name,
email: acct.email,
};
}
public static create(apiUrl: string): AuthProvider {
return new PortalAuthProvider(apiUrl);
}
}
interface RequiredAuthProvider extends AuthProvider {
login: AuthProvider['login'];
logout: AuthProvider['logout'];
check: AuthProvider['check'];
onError: AuthProvider['onError'];
register: AuthProvider['register'];
forgotPassword: AuthProvider['forgotPassword'];
updatePassword: AuthProvider['updatePassword'];
getPermissions: AuthProvider['getPermissions'];
getIdentity: AuthProvider['getIdentity'];
} }

View File

@ -12,7 +12,7 @@ import type { LinksFunction } from "@remix-run/node";
// Supports weights 200-800 // Supports weights 200-800
import '@fontsource-variable/manrope'; import '@fontsource-variable/manrope';
import {Refine} from "@refinedev/core"; import {Refine} from "@refinedev/core";
import {authProvider} from "~/data/auth-provider.js"; import {PortalAuthProvider} from "~/data/auth-provider.js";
import routerProvider from "@refinedev/remix-router"; import routerProvider from "@refinedev/remix-router";
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
@ -40,7 +40,7 @@ export function Layout({children}: { children: React.ReactNode }) {
export default function App() { export default function App() {
return ( return (
<Refine <Refine
authProvider={authProvider} authProvider={PortalAuthProvider.create("https://alpha.pinner.xyz")}
routerProvider={routerProvider} routerProvider={routerProvider}
> >
<Outlet/> <Outlet/>

View File

@ -1,25 +1,24 @@
import Login from "./login"; import {useGo, useIsAuthenticated} from "@refinedev/core";
import { useGo, useIsAuthenticated } from "@refinedev/core"; import {useEffect} from "react";
import { useEffect } from "react";
export default function Index() { export default function Index() {
const { isLoading, data } = useIsAuthenticated(); const {isLoading, data} = useIsAuthenticated();
const go = useGo(); const go = useGo();
useEffect(() => { useEffect(() => {
if (!isLoading && data?.authenticated) { if (!isLoading) {
go({ to: "/dashboard", type: "replace" }); if (data?.authenticated) {
go({to: "/dashboard", type: "replace"});
} else {
go({to: "/login", type: "replace"});
}
}
}, [isLoading, data]);
if (isLoading) {
return <>Checking Login Status</> || null;
} }
}, [isLoading, data]);
if (isLoading) { return (<>Redirecting</>) || null;
return <>Checking Login Status</> || null;
}
if (data?.authenticated) {
return <>Redirecting</> || null;
}
return <Login />;
} }

View File

@ -1,71 +1,69 @@
import { GeneralLayout } from "~/components/general-layout" import { GeneralLayout } from "~/components/general-layout";
import { import {
CloudIcon, CloudIcon,
CloudDownloadIcon, CloudDownloadIcon,
CloudUploadSolidIcon CloudUploadSolidIcon,
} from "~/components/icons" } from "~/components/icons";
import { UpgradeAccountBanner } from "~/components/upgrade-account-banner" import { UpgradeAccountBanner } from "~/components/upgrade-account-banner";
import { UsageCard } from "~/components/usage-card" import { UsageCard } from "~/components/usage-card";
import { UsageChart } from "~/components/usage-chart" import { UsageChart } from "~/components/usage-chart";
import { Authenticated } from "@refinedev/core";
export default function Dashboard() { export default function Dashboard() {
const isLogged = true
if (!isLogged) {
window.location.href = "/login"
}
return ( return (
<GeneralLayout> <Authenticated key="dashboard" v3LegacyAuthProviderCompatible>
<h1 className="font-bold mb-4 text-3xl">Dashboard</h1> <GeneralLayout>
<UpgradeAccountBanner /> <h1 className="font-bold mb-4 text-3xl">Dashboard</h1>
<h2 className="font-bold mb-8 mt-10 text-2xl">Current Usage</h2> <UpgradeAccountBanner />
<div className="grid grid-cols-2 gap-8"> <h2 className="font-bold mb-8 mt-10 text-2xl">Current Usage</h2>
<UsageCard <div className="grid grid-cols-2 gap-8">
label="Storage" <UsageCard
currentUsage={120} label="Storage"
monthlyUsage={130} currentUsage={120}
icon={<CloudIcon className="text-ring" />} monthlyUsage={130}
/> icon={<CloudIcon className="text-ring" />}
<UsageCard />
label="Download" <UsageCard
currentUsage={2} label="Download"
monthlyUsage={10} currentUsage={2}
icon={<CloudDownloadIcon className="text-ring" />} monthlyUsage={10}
/> icon={<CloudDownloadIcon className="text-ring" />}
<UsageCard />
label="Upload" <UsageCard
currentUsage={5} label="Upload"
monthlyUsage={15} currentUsage={5}
icon={<CloudUploadSolidIcon className="text-ring" />} monthlyUsage={15}
/> icon={<CloudUploadSolidIcon className="text-ring" />}
</div> />
<h2 className="font-bold mb-8 mt-10 text-2xl">Historical Usage</h2> </div>
<div className="grid gap-8 grid-cols-2"> <h2 className="font-bold mb-8 mt-10 text-2xl">Historical Usage</h2>
<UsageChart <div className="grid gap-8 grid-cols-2">
dataset={[ <UsageChart
{ x: "3/2", y: "50" }, dataset={[
{ x: "3/3", y: "10" }, { x: "3/2", y: "50" },
{ x: "3/4", y: "20" } { x: "3/3", y: "10" },
]} { x: "3/4", y: "20" },
label="Storage" ]}
/> label="Storage"
<UsageChart />
dataset={[ <UsageChart
{ x: "3/2", y: "50" }, dataset={[
{ x: "3/3", y: "10" }, { x: "3/2", y: "50" },
{ x: "3/4", y: "20" } { x: "3/3", y: "10" },
]} { x: "3/4", y: "20" },
label="Download" ]}
/> label="Download"
<UsageChart />
dataset={[ <UsageChart
{ x: "3/2", y: "50" }, dataset={[
{ x: "3/3", y: "10" }, { x: "3/2", y: "50" },
{ x: "3/4", y: "20" } { x: "3/3", y: "10" },
]} { x: "3/4", y: "20" },
label="Upload" ]}
/> label="Upload"
</div> />
</GeneralLayout> </div>
) </GeneralLayout>
</Authenticated>
);
} }

View File

@ -1,179 +1,215 @@
import type { MetaFunction } from "@remix-run/node" import type {MetaFunction} from "@remix-run/node"
import { Link, useLocation } from "@remix-run/react" import {Link, useLocation} from "@remix-run/react"
import { z } from "zod" import {z} from "zod"
import { Button } from "~/components/ui/button" import {Button} from "~/components/ui/button"
import logoPng from "~/images/lume-logo.png?url" import logoPng from "~/images/lume-logo.png?url"
import lumeColorLogoPng from "~/images/lume-color-logo.png?url" import lumeColorLogoPng from "~/images/lume-color-logo.png?url"
import discordLogoPng from "~/images/discord-logo.png?url" import discordLogoPng from "~/images/discord-logo.png?url"
import lumeBgPng from "~/images/lume-bg-image.png?url" import lumeBgPng from "~/images/lume-bg-image.png?url"
import { Field, FieldCheckbox } from "~/components/forms" import {Field, FieldCheckbox} from "~/components/forms"
import { getFormProps, useForm } from "@conform-to/react" import {getFormProps, useForm} from "@conform-to/react"
import { getZodConstraint, parseWithZod } from "@conform-to/zod" import {getZodConstraint, parseWithZod} from "@conform-to/zod"
import {useGo, useIsAuthenticated, useLogin, useParsed} from "@refinedev/core";
import {AuthFormRequest} from "~/data/auth-provider.js";
import {useEffect} from "react";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
{ title: "Login" }, {title: "Login"},
{ name: "description", content: "Welcome to Lume!" } {name: "description", content: "Welcome to Lume!"}
] ]
}
type LoginParams = {
to: string;
} }
export default function Login() { export default function Login() {
const location = useLocation() const location = useLocation()
const hash = location.hash const {isLoading: isAuthLoading, data: authData} = useIsAuthenticated();
const hash = location.hash
const go = useGo();
const parsed = useParsed<LoginParams>()
return ( useEffect(() => {
<div className="p-10 h-screen relative"> if (!isAuthLoading) {
<header> if (authData?.authenticated) {
<img src={logoPng} alt="Lume logo" className="h-10" /> let to = "/dashboard";
</header> if(parsed.params?.to){
<div className="fixed inset-0 -z-10 overflow-clip"> to = parsed.params.to
<img }
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
/>
</div>
{hash === "" && <LoginForm />} go({to, type: "push"});
{hash === "#otp" && <OtpForm />} }
}
}, [isAuthLoading, authData]);
<footer className="my-5"> return (
<ul className="flex flex-row"> <div className="p-10 h-screen relative">
<li> <header>
<Link to="https://discord.lumeweb.com"> <img src={logoPng} alt="Lume logo" className="h-10"/>
<Button </header>
variant={"link"} <div className="fixed inset-0 -z-10 overflow-clip">
className="flex flex-row gap-x-2 text-input-placeholder" <img
> src={lumeBgPng}
<img className="h-5" src={discordLogoPng} alt="Discord Logo" /> alt="Lume background"
Connect with us className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
</Button> />
</Link> </div>
</li>
<li> {hash === "" && <LoginForm/>}
<Link to="https://lumeweb.com"> {hash === "#otp" && <OtpForm/>}
<Button
variant={"link"} <footer className="my-5">
className="flex flex-row gap-x-2 text-input-placeholder" <ul className="flex flex-row">
> <li>
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" /> <Link to="https://discord.lumeweb.com">
Connect with us <Button
</Button> variant={"link"}
</Link> className="flex flex-row gap-x-2 text-input-placeholder"
</li> >
</ul> <img className="h-5" src={discordLogoPng} alt="Discord Logo"/>
</footer> Connect with us
</div> </Button>
) </Link>
</li>
<li>
<Link to="https://lumeweb.com">
<Button
variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder"
>
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo"/>
Connect with us
</Button>
</Link>
</li>
</ul>
</footer>
</div>
)
} }
const LoginSchema = z.object({ const LoginSchema = z.object({
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string(),
rememberMe: z.boolean() rememberMe: z.boolean()
}) })
const LoginForm = () => { const LoginForm = () => {
const [form, fields] = useForm({ const login = useLogin<AuthFormRequest>()
id: "login", const parsed = useParsed<LoginParams>()
constraint: getZodConstraint(LoginSchema), const [form, fields] = useForm({
onValidate({ formData }) { id: "login",
return parseWithZod(formData, { schema: LoginSchema }) constraint: getZodConstraint(LoginSchema),
}, onValidate({formData}) {
shouldValidate: "onSubmit" return parseWithZod(formData, {schema: LoginSchema})
}) },
shouldValidate: "onSubmit",
onSubmit(e) {
e.preventDefault();
return ( const data = Object.fromEntries(new FormData(e.currentTarget).entries());
<form login.mutate({
className="w-full p-2 max-w-md space-y-3 mt-12 bg-background" email: data.email.toString(),
{...getFormProps(form)} password: data.password.toString(),
> rememberMe: data.rememberMe.toString() === "on",
<h2 className="text-3xl font-bold !mb-12">Welcome back! 🎉</h2> redirectTo: parsed.params?.to
<Field });
inputProps={{ name: fields.email.name }} }
labelProps={{ children: "Email" }} })
errors={fields.email.errors}
/> return (
<Field <form
inputProps={{ name: fields.password.name, type: "password" }} className="w-full p-2 max-w-md space-y-3 mt-12 bg-background"
labelProps={{ children: "Password" }} {...getFormProps(form)}
errors={fields.password.errors}
/>
<FieldCheckbox
inputProps={{ name: fields.rememberMe.name, form: form.id }}
labelProps={{ children: "Remember Me" }}
errors={fields.rememberMe.errors}
/>
<Button className="w-full h-14">Login</Button>
<p className="inline-block text-input-placeholder">
Forgot your password?{" "}
<Link
to="/reset-password"
className="text-primary-1 text-md hover:underline hover:underline-offset-4"
> >
Reset Password <h2 className="text-3xl font-bold !mb-12">Welcome back! 🎉</h2>
</Link> <Field
</p> inputProps={{name: fields.email.name}}
<Link to="/sign-up" className="block"> labelProps={{children: "Email"}}
<Button type="button" className="w-full h-14" variant={"outline"}> errors={fields.email.errors}
Create an Account />
</Button> <Field
</Link> inputProps={{name: fields.password.name, type: "password"}}
</form> labelProps={{children: "Password"}}
) errors={fields.password.errors}
/>
<FieldCheckbox
inputProps={{name: fields.rememberMe.name, form: form.id}}
labelProps={{children: "Remember Me"}}
errors={fields.rememberMe.errors}
/>
<Button className="w-full h-14">Login</Button>
<p className="inline-block text-input-placeholder">
Forgot your password?{" "}
<Link
to="/reset-password"
className="text-primary-1 text-md hover:underline hover:underline-offset-4"
>
Reset Password
</Link>
</p>
<Link to="/register" className="block">
<Button type="button" className="w-full h-14" variant={"outline"}>
Create an Account
</Button>
</Link>
</form>
)
} }
const OtpSchema = z.object({ const OtpSchema = z.object({
otp: z.string().length(6, { message: "OTP must be 6 characters" }) otp: z.string().length(6, {message: "OTP must be 6 characters"})
}) })
const OtpForm = () => { const OtpForm = () => {
// TODO: Add support for resending the OTP // TODO: Add support for resending the OTP
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "otp", id: "otp",
constraint: getZodConstraint(OtpSchema), constraint: getZodConstraint(OtpSchema),
onValidate({ formData }) { onValidate({formData}) {
return parseWithZod(formData, { schema: OtpSchema }) return parseWithZod(formData, {schema: OtpSchema})
}, },
shouldValidate: "onSubmit" shouldValidate: "onSubmit"
}) })
const valid = false // TODO: some sort of logic to verify user is on OTP state validly const valid = false // TODO: some sort of logic to verify user is on OTP state validly
if (!valid) { if (!valid) {
location.hash = "" location.hash = ""
return null return null
} }
return ( return (
<form <form
className="w-full p-2 max-w-md mt-12 bg-background" className="w-full p-2 max-w-md mt-12 bg-background"
{...getFormProps(form)} {...getFormProps(form)}
> >
<span className="block !mb-8 space-y-2"> <span className="block !mb-8 space-y-2">
<h2 className="text-3xl font-bold">Check your inbox</h2> <h2 className="text-3xl font-bold">Check your inbox</h2>
<p className="text-input-placeholder"> <p className="text-input-placeholder">
We will need the six digit confirmation code you received in your We will need the six digit confirmation code you received in your
email in order to verify your account and get started. Didnt receive email in order to verify your account and get started. Didnt receive
a code?{" "} a code?{" "}
<Button type="button" variant={"link"} className="text-md h-0"> <Button type="button" variant={"link"} className="text-md h-0">
Resend now Resend now
</Button> </Button>
</p> </p>
</span> </span>
<Field <Field
inputProps={{ name: fields.otp.name }} inputProps={{name: fields.otp.name}}
labelProps={{ children: "Confirmation Code" }} labelProps={{children: "Confirmation Code"}}
errors={fields.otp.errors} errors={fields.otp.errors}
/> />
<Button className="w-full h-14">Verify</Button> <Button className="w-full h-14">Verify</Button>
<p className="text-input-placeholder w-full text-left"> <p className="text-input-placeholder w-full text-left">
<Link <Link
to="/login" to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4" className="text-primary-1 text-md hover:underline hover:underline-offset-4"
> >
Back to Login Back to Login
</Link> </Link>
</p> </p>
</form> </form>
) )
} }

View File

@ -9,13 +9,17 @@ import { Field, FieldCheckbox } from "~/components/forms"
import { getFormProps, useForm } from "@conform-to/react" import { getFormProps, useForm } from "@conform-to/react"
import { z } from "zod" import { z } from "zod"
import { getZodConstraint, parseWithZod } from "@conform-to/zod" import { getZodConstraint, parseWithZod } from "@conform-to/zod"
import {useLogin, useRegister} from "@refinedev/core";
import {AuthFormRequest, RegisterFormRequest} from "~/data/auth-provider.js";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [{ title: "Sign Up" }] return [{ title: "Sign Up" }];
} };
const SignUpSchema = z const RegisterSchema = z
.object({ .object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(), email: z.string().email(),
password: z password: z
.string() .string()
@ -24,28 +28,49 @@ const SignUpSchema = z
.string() .string()
.min(8, { message: "Password must be at least 8 characters" }), .min(8, { message: "Password must be at least 8 characters" }),
termsOfService: z.boolean({ termsOfService: z.boolean({
required_error: "You must agree to the terms of service" required_error: "You must agree to the terms of service",
}) }),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) { if (data.password !== data.confirmPassword) {
return ctx.addIssue({ return ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ["confirmPassword"], path: ["confirmPassword"],
message: "Passwords do not match" message: "Passwords do not match",
}) });
} }
return true return true;
}) });
export default function SignUp() { export default function Register() {
const register = useRegister<RegisterFormRequest>()
const login = useLogin<AuthFormRequest>();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "sign-up", id: "register",
constraint: getZodConstraint(SignUpSchema), constraint: getZodConstraint(RegisterSchema),
onValidate({ formData }) { onValidate({ formData }) {
return parseWithZod(formData, { schema: SignUpSchema }) return parseWithZod(formData, { schema: RegisterSchema });
} },
}) onSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
register.mutate({
email: data.email.toString(),
password: data.password.toString(),
firstName: data.firstName.toString(),
lastName: data.lastName.toString(),
}, {
onSuccess: () => {
login.mutate({
email: data.email.toString(),
password: data.password.toString(),
rememberMe: false,
})
}
})
}
});
return ( return (
<div className="p-10 h-screen relative"> <div className="p-10 h-screen relative">
@ -54,21 +79,33 @@ export default function SignUp() {
</header> </header>
<form <form
className="w-full p-2 max-w-md space-y-4 mt-12 bg-background" className="w-full p-2 max-w-md space-y-4 mt-12 bg-background"
{...getFormProps(form)} {...getFormProps(form)}>
>
<span className="!mb-12 space-y-2"> <span className="!mb-12 space-y-2">
<h2 className="text-3xl font-bold">All roads lead to Lume</h2> <h2 className="text-3xl font-bold">All roads lead to Lume</h2>
<p className="text-input-placeholder"> <p className="text-input-placeholder">
🤘 Get 50 GB free storage and download for free,{" "} 🤘 Get 50 GB free storage and download for free,{" "}
<b <b
className="text-primar className="text-primar
y-2" y-2">
>
forever forever
</b> </b>
.{" "} .{" "}
</p> </p>
</span> </span>
<div className="flex gap-4">
<Field
className="flex-1"
inputProps={{ name: fields.firstName.name }}
labelProps={{ children: "First Name" }}
errors={fields.firstName.errors}
/>
<Field
className="flex-1"
inputProps={{ name: fields.lastName.name }}
labelProps={{ children: "Last Name" }}
errors={fields.lastName.errors}
/>
</div>
<Field <Field
inputProps={{ name: fields.email.name }} inputProps={{ name: fields.email.name }}
labelProps={{ children: "Email" }} labelProps={{ children: "Email" }}
@ -92,19 +129,17 @@ export default function SignUp() {
I agree to the I agree to the
<Link <Link
to="/terms-of-service" to="/terms-of-service"
className="text-primary-1 text-md hover:underline hover:underline-offset-4 mx-1" className="text-primary-1 text-md hover:underline hover:underline-offset-4 mx-1">
>
Terms of Service Terms of Service
</Link> </Link>
and and
<Link <Link
to="/privacy-policy" to="/privacy-policy"
className="text-primary-1 text-md hover:underline hover:underline-offset-4 mx-1" className="text-primary-1 text-md hover:underline hover:underline-offset-4 mx-1">
>
Privacy Policy Privacy Policy
</Link> </Link>
</span> </span>
) ),
}} }}
errors={fields.termsOfService.errors} errors={fields.termsOfService.errors}
/> />
@ -113,8 +148,7 @@ export default function SignUp() {
Already have an account?{" "} Already have an account?{" "}
<Link <Link
to="/login" to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4" className="text-primary-1 text-md hover:underline hover:underline-offset-4">
>
Login here instead Login here instead
</Link> </Link>
</p> </p>
@ -132,8 +166,7 @@ export default function SignUp() {
<Link to="https://discord.lumeweb.com"> <Link to="https://discord.lumeweb.com">
<Button <Button
variant={"link"} variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder" className="flex flex-row gap-x-2 text-input-placeholder">
>
<img className="h-5" src={discordLogoPng} alt="Discord Logo" /> <img className="h-5" src={discordLogoPng} alt="Discord Logo" />
Connect with us Connect with us
</Button> </Button>
@ -143,8 +176,7 @@ export default function SignUp() {
<Link to="https://lumeweb.com"> <Link to="https://lumeweb.com">
<Button <Button
variant={"link"} variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder" className="flex flex-row gap-x-2 text-input-placeholder">
>
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" /> <img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
Connect with us Connect with us
</Button> </Button>
@ -153,5 +185,5 @@ export default function SignUp() {
</ul> </ul>
</footer> </footer>
</div> </div>
) );
} }

View File

@ -1,31 +1,36 @@
import type { MetaFunction } from "@remix-run/node" import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react" import { Link } from "@remix-run/react";
import { Button } from "~/components/ui/button" import { Button } from "~/components/ui/button";
import logoPng from "~/images/lume-logo.png?url" import logoPng from "~/images/lume-logo.png?url";
import lumeColorLogoPng from "~/images/lume-color-logo.png?url" import lumeColorLogoPng from "~/images/lume-color-logo.png?url";
import discordLogoPng from "~/images/discord-logo.png?url" import discordLogoPng from "~/images/discord-logo.png?url";
import lumeBgPng from "~/images/lume-bg-image.png?url" import lumeBgPng from "~/images/lume-bg-image.png?url";
import { Field } from "~/components/forms" import { Field } from "~/components/forms";
import { getFormProps, useForm } from "@conform-to/react" import { getFormProps, useForm } from "@conform-to/react";
import { z } from "zod" import { z } from "zod";
import { getZodConstraint, parseWithZod } from "@conform-to/zod" import { getZodConstraint, parseWithZod } from "@conform-to/zod";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [{ title: "Sign Up" }] return [{ title: "Sign Up" }];
} };
const RecoverPasswordSchema = z const RecoverPasswordSchema = z.object({
.object({ email: z.string().email(),
email: z.string().email(), });
})
export default function RecoverPassword() { export default function RecoverPassword() {
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "sign-up", id: "sign-up",
constraint: getZodConstraint(RecoverPasswordSchema), constraint: getZodConstraint(RecoverPasswordSchema),
onValidate({ formData }) { onValidate({ formData }) {
return parseWithZod(formData, { schema: RecoverPasswordSchema }) return parseWithZod(formData, { schema: RecoverPasswordSchema });
} },
}) });
// TODO: another detail is the reset password has no screen to either accept a new pass or
// just say an email has been sent.. if i were to generate a pass for them. imho i think
// a screen that just says a password reset email has been sent would be good, then a separate
// route to accept the reset token and send that to the api when would then trigger a new email
// with the pass.
return ( return (
<div className="p-10 h-screen relative"> <div className="p-10 h-screen relative">
@ -34,8 +39,7 @@ export default function RecoverPassword() {
</header> </header>
<form <form
className="w-full p-2 max-w-md space-y-4 mt-12 bg-background" className="w-full p-2 max-w-md space-y-4 mt-12 bg-background"
{...getFormProps(form)} {...getFormProps(form)}>
>
<span className="!mb-12 space-y-2"> <span className="!mb-12 space-y-2">
<h2 className="text-3xl font-bold">Reset your password</h2> <h2 className="text-3xl font-bold">Reset your password</h2>
</span> </span>
@ -44,13 +48,12 @@ export default function RecoverPassword() {
labelProps={{ children: "Email Address" }} labelProps={{ children: "Email Address" }}
errors={fields.email.errors} errors={fields.email.errors}
/> />
<Button className="w-full h-14">Create Account</Button> <Button className="w-full h-14">Reset Password</Button>
<p className="text-input-placeholder w-full text-left"> <p className="text-input-placeholder w-full text-left">
<Link <Link
to="/login" to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4" className="text-primary-1 text-md hover:underline hover:underline-offset-4">
> Back to Login
Back to Login
</Link> </Link>
</p> </p>
</form> </form>
@ -67,8 +70,7 @@ export default function RecoverPassword() {
<Link to="https://discord.lumeweb.com"> <Link to="https://discord.lumeweb.com">
<Button <Button
variant={"link"} variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder" className="flex flex-row gap-x-2 text-input-placeholder">
>
<img className="h-5" src={discordLogoPng} alt="Discord Logo" /> <img className="h-5" src={discordLogoPng} alt="Discord Logo" />
Connect with us Connect with us
</Button> </Button>
@ -78,8 +80,7 @@ export default function RecoverPassword() {
<Link to="https://lumeweb.com"> <Link to="https://lumeweb.com">
<Button <Button
variant={"link"} variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder" className="flex flex-row gap-x-2 text-input-placeholder">
>
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" /> <img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
Connect with us Connect with us
</Button> </Button>
@ -88,5 +89,5 @@ export default function RecoverPassword() {
</ul> </ul>
</footer> </footer>
</div> </div>
) );
} }

19971
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
"@conform-to/react": "^1.0.2", "@conform-to/react": "^1.0.2",
"@conform-to/zod": "^1.0.2", "@conform-to/zod": "^1.0.2",
"@fontsource-variable/manrope": "^5.0.19", "@fontsource-variable/manrope": "^5.0.19",
"@lumeweb/portal-sdk": "^0.0.0-20240306231947", "@lumeweb/portal-sdk": "0.0.0-20240314110748",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
@ -39,6 +39,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",