create sig in flow LO
This commit is contained in:
@@ -1,24 +1,133 @@
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { Button } from "../ui/button";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRef, useState } from "react";
|
||||
import Arrow from "@/icons/arrow.svg?react";
|
||||
import Paperclip from "@/icons/paperclip.svg?react";
|
||||
import Clock from "@/icons/clock.svg?react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Chat = observer(() => {
|
||||
const { isSandboxOpen, setIsSandboxOpen, isMainMenuOpen, setIsMainMenuOpen } =
|
||||
useAccountStore();
|
||||
|
||||
const Chat = observer(() => {
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSandboxOpen(!isSandboxOpen);
|
||||
if (isMainMenuOpen && !isSandboxOpen) {
|
||||
setIsMainMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
className="ml-10"
|
||||
>
|
||||
OPEN SANDBOX
|
||||
</Button>
|
||||
<div className="flex-1 h-full flex items-center justify-center">
|
||||
<WelcomePanel />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const WelcomePanel = () => {
|
||||
return (
|
||||
<div className="max-w-[741px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2 font-medium">
|
||||
<span className="leading-[120%] text-black text-[24px]">
|
||||
Hi!
|
||||
<br /> I will help you create a crypto project on Cytonic
|
||||
</span>
|
||||
<span className="text-[#C0C0C0] text-[14px] flex gap-2 items-center">
|
||||
<Clock />
|
||||
<p>Estimated build time: 3 minutes</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-[#C0C0C0] text-[14px] font-medium leading-[120%]">
|
||||
Choose one of the most common prompts
|
||||
<br /> below or use your own to begin
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{[
|
||||
{ emoji: "🐶", text: "Build a memecoin launchpad" },
|
||||
{ emoji: "🏦", text: "Build a new stablecoin protocol" },
|
||||
{ emoji: "🌐", text: "Create a RWA marketplace on Cytonic" },
|
||||
{ emoji: "📊", text: "Ship a token dashboard" },
|
||||
].map(({ emoji, text }) => (
|
||||
<button
|
||||
type="button"
|
||||
key={emoji}
|
||||
className="text-left cursor-pointer hover:bg-fill-100 transition-all w-[173px] h-[123px] p-4 rounded-[16px] border border-[#E8E8E8] flex flex-col justify-between"
|
||||
>
|
||||
<p className="text-[14px] font-medium leading-[120%]">{text}</p>
|
||||
<div className="text-[24px] leading-[100%]">{emoji}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ChatInput />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatInput = () => {
|
||||
const [message, setMessage] = useState("");
|
||||
const [showShadow, setShowShadow] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const el = e.target;
|
||||
setMessage(el.value);
|
||||
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
|
||||
|
||||
setShowShadow(el.scrollTop + el.clientHeight < el.scrollHeight);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = textareaRef.current;
|
||||
if (!el) return;
|
||||
setShowShadow(el.scrollTop + el.clientHeight < el.scrollHeight);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative border border-[#E8E8E8] rounded-[16px] pt-4 pb-2 pl-4 pr-2 flex flex-col justify-between transition-all duration-200"
|
||||
style={{
|
||||
boxShadow: "0px 1px 8px 0px #00000012",
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
onInput={handleInput}
|
||||
onScroll={handleScroll}
|
||||
className="text-[14px] placeholder:text-[#818181] focus:outline-none overflow-y-auto min-h-[40px] max-h-[150px] resize-none w-full pr-4"
|
||||
placeholder="Ask whatever you want..."
|
||||
value={message}
|
||||
rows={1}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 bottom-0 w-full h-5 pointer-events-none transition-opacity duration-300",
|
||||
showShadow ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0.95), #ffffff)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<div className="flex gap-6">
|
||||
<span className="flex gap-2 items-center cursor-pointer">
|
||||
<Paperclip />
|
||||
<p className="text-[14px] text-[#C0C0C0]">Add attachment</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={message.trim() === ""}
|
||||
className="w-10 h-10 rounded-[8px] flex items-center justify-center rotate-180 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ backgroundColor: "#E8E8E8" }}
|
||||
>
|
||||
<Arrow color="white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Chat, WelcomePanel, ChatInput };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from "../ui/button";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export const MainMenu = observer(() => {
|
||||
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap rounded-[8px] px-4 text-[14px] leading-[130%] tracking-[0.01em] transition-colors hover:cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:*:fill-text-secondary [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"h-[52px] inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap rounded-[8px] px-4 text-[14px] leading-[130%] tracking-[0.01em] transition-colors hover:cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:*:fill-text-secondary [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
62
src/components/ui/input.tsx
Normal file
62
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { HTMLInputTypeAttribute } from "react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type InputProps = Omit<React.ComponentProps<"input">, "type"> & {
|
||||
type?: HTMLInputTypeAttribute | "token";
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
label?: string;
|
||||
|
||||
rightContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ children, className, type, label, required, ...props }, ref) => {
|
||||
return (
|
||||
<div className="group flex w-full flex-col gap-3">
|
||||
{label && (
|
||||
<span
|
||||
data-required={required}
|
||||
className="text-text-light-500 font-[500] text-[12px] after:ml-0.5 after:text-negative-primary data-[required=true]:after:content-['*']"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"group flex rounded-[8px] items-center w-full border transition-colors border-fill-200 hover:border-fill-400",
|
||||
"focus-within:border-fill-400",
|
||||
"disabled:border-b-fill-secondary",
|
||||
"data-[invalid=true]:border-b-negative-primary",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type={type}
|
||||
ref={ref}
|
||||
data-disabled={props.disabled}
|
||||
className={cn(
|
||||
"text-[14px] px-4 py-3 flex w-full border-none bg-transparent transition-colors placeholder:text-text-secondary",
|
||||
"focus:placeholder:opacity-0 disabled:cursor-not-allowed disabled:text-text-quartinary",
|
||||
"data-[disabled=true]:placeholder:text-text-quinary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{props.rightContent && (
|
||||
<div className="pr-2 pb-2 flex items-center gap-1 text-text-primary text-sm">
|
||||
{props.rightContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
3
src/icons/arrow.svg
Normal file
3
src/icons/arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.219375 8.53104L6.96937 15.281C7.11011 15.4218 7.30098 15.5008 7.5 15.5008C7.69902 15.5008 7.88989 15.4218 8.03063 15.281C8.17136 15.1403 8.25042 14.9494 8.25042 14.7504C8.25042 14.5514 8.17136 14.3605 8.03063 14.2198L2.56031 8.75042H17.25C17.4489 8.75042 17.6397 8.6714 17.7803 8.53075C17.921 8.3901 18 8.19933 18 8.00042C18 7.8015 17.921 7.61074 17.7803 7.47009C17.6397 7.32943 17.4489 7.25042 17.25 7.25042H2.56031L8.03063 1.78104C8.17136 1.64031 8.25042 1.44944 8.25042 1.25042C8.25042 1.05139 8.17136 0.860523 8.03063 0.719792C7.88989 0.579062 7.69902 0.5 7.5 0.5C7.30098 0.5 7.11011 0.579062 6.96937 0.719792L0.219375 7.46979C0.149642 7.53945 0.0943228 7.62216 0.0565796 7.71321C0.0188364 7.80426 -0.000589371 7.90186 -0.000589371 8.00042C-0.000589371 8.09898 0.0188364 8.19657 0.0565796 8.28762C0.0943228 8.37867 0.149642 8.46139 0.219375 8.53104Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 992 B |
3
src/icons/clock.svg
Normal file
3
src/icons/clock.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 0.25C8.07164 0.25 6.18657 0.821828 4.58319 1.89317C2.97982 2.96452 1.73013 4.48726 0.992179 6.26884C0.254224 8.05042 0.061142 10.0108 0.437348 11.9021C0.813554 13.7934 1.74215 15.5307 3.10571 16.8943C4.46928 18.2579 6.20656 19.1865 8.09787 19.5627C9.98919 19.9389 11.9496 19.7458 13.7312 19.0078C15.5127 18.2699 17.0355 17.0202 18.1068 15.4168C19.1782 13.8134 19.75 11.9284 19.75 10C19.7473 7.41498 18.7192 4.93661 16.8913 3.10872C15.0634 1.28084 12.585 0.25273 10 0.25ZM10 18.25C8.36831 18.25 6.77326 17.7661 5.41655 16.8596C4.05984 15.9531 3.00242 14.6646 2.378 13.1571C1.75358 11.6496 1.5902 9.99085 1.90853 8.3905C2.22685 6.79016 3.01259 5.32015 4.16637 4.16637C5.32016 3.01259 6.79017 2.22685 8.39051 1.90852C9.99085 1.59019 11.6497 1.75357 13.1571 2.37799C14.6646 3.00242 15.9531 4.05984 16.8596 5.41655C17.7662 6.77325 18.25 8.3683 18.25 10C18.2475 12.1873 17.3775 14.2843 15.8309 15.8309C14.2843 17.3775 12.1873 18.2475 10 18.25ZM16 10C16 10.1989 15.921 10.3897 15.7803 10.5303C15.6397 10.671 15.4489 10.75 15.25 10.75H10C9.80109 10.75 9.61033 10.671 9.46967 10.5303C9.32902 10.3897 9.25 10.1989 9.25 10V4.75C9.25 4.55109 9.32902 4.36032 9.46967 4.21967C9.61033 4.07902 9.80109 4 10 4C10.1989 4 10.3897 4.07902 10.5303 4.21967C10.671 4.36032 10.75 4.55109 10.75 4.75V9.25H15.25C15.4489 9.25 15.6397 9.32902 15.7803 9.46967C15.921 9.61032 16 9.80109 16 10Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
10
src/icons/logo-with-text.svg
Normal file
10
src/icons/logo-with-text.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="123" height="25" viewBox="0 0 123 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21611 1.09424C7.96286 0.347493 9.17358 0.347492 9.92033 1.09424L10.9714 2.14531C11.511 2.68494 12.386 2.68494 12.9256 2.1453L13.9767 1.09424C14.7234 0.347492 15.9341 0.347492 16.6809 1.09424L23.3374 7.75078C24.0842 8.49753 24.0842 9.70825 23.3374 10.455L22.2863 11.5061C21.7467 12.0457 21.7467 12.9206 22.2863 13.4603L23.3374 14.5113C24.0842 15.2581 24.0842 16.4688 23.3374 17.2155L16.6809 23.8721C15.9341 24.6188 14.7234 24.6188 13.9767 23.8721L12.9256 22.821C12.386 22.2814 11.511 22.2814 10.9714 22.821L9.92033 23.8721C9.17358 24.6188 7.96286 24.6188 7.21611 23.8721L0.559574 17.2155C-0.187175 16.4688 -0.187175 15.2581 0.559574 14.5113L1.61064 13.4603C2.15027 12.9206 2.15027 12.0457 1.61064 11.5061L0.559574 10.455C-0.187176 9.70825 -0.187176 8.49753 0.559574 7.75078L7.21611 1.09424ZM10.9714 20.5548C11.511 21.0944 12.386 21.0944 12.9256 20.5548L20.0201 13.4603C20.5598 12.9206 20.5598 12.0457 20.0201 11.5061L12.9256 4.41153C12.386 3.87189 11.511 3.87189 10.9714 4.41153L3.87686 11.5061C3.33722 12.0457 3.33722 12.9206 3.87686 13.4603L10.9714 20.5548Z" fill="#202020"/>
|
||||
<path d="M104.238 18.5163V5.66672H106.911V18.5163H104.238ZM105.587 3.92044C105.068 3.92044 104.627 3.75537 104.264 3.42522C103.918 3.0777 103.745 2.63462 103.745 2.09596C103.745 1.55731 103.918 1.12291 104.264 0.792765C104.627 0.445245 105.068 0.271484 105.587 0.271484C106.124 0.271484 106.565 0.445245 106.911 0.792765C107.257 1.12291 107.43 1.55731 107.43 2.09596C107.43 2.63462 107.257 3.0777 106.911 3.42522C106.565 3.75537 106.124 3.92044 105.587 3.92044Z" fill="#202020"/>
|
||||
<path d="M89.0493 18.5161V5.66652H91.6706V7.59525H92.0858C92.3281 7.07397 92.7606 6.58745 93.3835 6.13567C94.0064 5.6839 94.932 5.45801 96.1605 5.45801C97.1294 5.45801 97.9858 5.67521 98.7298 6.10961C99.4911 6.54401 100.088 7.16085 100.521 7.96015C100.953 8.74207 101.169 9.68906 101.169 10.8011V18.5161H98.4963V11.0096C98.4963 9.89757 98.2194 9.0809 97.6658 8.55962C97.1121 8.02096 96.3508 7.75164 95.3819 7.75164C94.2745 7.75164 93.3835 8.11653 92.7087 8.84632C92.0512 9.57611 91.7225 10.636 91.7225 12.0261V18.5161H89.0493Z" fill="#202020"/>
|
||||
<path d="M79.5956 18.8811C78.3152 18.8811 77.1733 18.6204 76.1698 18.0992C75.1836 17.5605 74.405 16.796 73.834 15.8055C73.263 14.8151 72.9775 13.6422 72.9775 12.2869V11.8959C72.9775 10.5406 73.263 9.37642 73.834 8.40337C74.405 7.41294 75.1836 6.6484 76.1698 6.10974C77.1733 5.57109 78.3152 5.30176 79.5956 5.30176C80.8759 5.30176 82.0179 5.57109 83.0214 6.10974C84.0249 6.6484 84.8122 7.41294 85.3831 8.40337C85.9541 9.37642 86.2396 10.5406 86.2396 11.8959V12.2869C86.2396 13.6422 85.9541 14.8151 85.3831 15.8055C84.8122 16.796 84.0249 17.5605 83.0214 18.0992C82.0179 18.6204 80.8759 18.8811 79.5956 18.8811ZM79.5956 16.4832C80.7721 16.4832 81.7237 16.1096 82.4504 15.3624C83.1944 14.5979 83.5664 13.5467 83.5664 12.2087V11.9741C83.5664 10.6362 83.2031 9.59362 82.4764 8.84646C81.7497 8.08191 80.7894 7.69964 79.5956 7.69964C78.4363 7.69964 77.4847 8.08191 76.7407 8.84646C76.0141 9.59362 75.6507 10.6362 75.6507 11.9741V12.2087C75.6507 13.5467 76.0141 14.5979 76.7407 15.3624C77.4847 16.1096 78.4363 16.4832 79.5956 16.4832Z" fill="#202020"/>
|
||||
<path d="M67.7495 18.5159C66.9709 18.5159 66.348 18.2813 65.8809 17.8122C65.431 17.343 65.2061 16.7175 65.2061 15.9356V7.93396H62.4551V5.66639H65.2061V1.41797H67.8793V5.66639H71.6944V7.93396H67.8793V15.4664C67.8793 15.9877 68.1215 16.2484 68.606 16.2484H71.2791V18.5159H67.7495Z" fill="#202020"/>
|
||||
<path d="M49.5246 23.7283V21.3826H56.6617C57.1462 21.3826 57.3884 21.1219 57.3884 20.6007V16.6389H56.9732C56.8175 16.9865 56.5752 17.3253 56.2465 17.6554C55.935 17.9682 55.5111 18.2288 54.9748 18.4374C54.4384 18.6459 53.7636 18.7501 52.9504 18.7501C51.9815 18.7501 51.1164 18.5329 50.3551 18.0985C49.5938 17.6641 48.9969 17.0473 48.5644 16.248C48.1318 15.4487 47.9155 14.5017 47.9155 13.407V5.66602H50.5887V13.1985C50.5887 14.3106 50.8655 15.1359 51.4192 15.6746C51.9729 16.1959 52.7428 16.4565 53.729 16.4565C54.8191 16.4565 55.6928 16.0916 56.3503 15.3618C57.0251 14.632 57.3625 13.5721 57.3625 12.182V5.66602H60.0356V21.148C60.0356 21.9299 59.8021 22.5555 59.3349 23.0246C58.8851 23.4938 58.2622 23.7283 57.4663 23.7283H49.5246Z" fill="#202020"/>
|
||||
<path d="M34.6252 17.0251C33.4018 15.7446 32.79 14.1227 32.79 12.1594C32.79 10.196 33.4018 8.57415 34.6252 7.29371C35.8486 6.01327 37.3737 5.37305 39.2004 5.37305C41.2451 5.37305 42.9126 6.16692 44.2031 7.75467C44.7561 8.48879 45.1416 9.27413 45.3594 10.1107H42.5942C42.4769 9.68387 42.259 9.29974 41.9406 8.95829C41.287 8.19002 40.3736 7.80589 39.2004 7.80589C38.1614 7.80589 37.2899 8.20709 36.586 9.0095C35.8989 9.81191 35.5553 10.8619 35.5553 12.1594C35.5553 13.4569 35.8989 14.5069 36.586 15.3093C37.2899 16.1117 38.1614 16.5129 39.2004 16.5129C40.4071 16.5129 41.354 16.1288 42.0411 15.3605C42.3428 15.019 42.569 14.6349 42.7199 14.2081H45.3594C45.1416 15.0788 44.7561 15.8727 44.2031 16.5897C42.9294 18.1604 41.2618 18.9457 39.2004 18.9457C37.3737 18.9457 35.8486 18.3055 34.6252 17.0251Z" fill="#202020"/>
|
||||
<path d="M111.446 17.0251C110.223 15.7446 109.611 14.1227 109.611 12.1594C109.611 10.196 110.223 8.57415 111.446 7.29371C112.67 6.01327 114.195 5.37305 116.022 5.37305C118.066 5.37305 119.734 6.16692 121.024 7.75467C121.577 8.48879 121.963 9.27413 122.181 10.1107H119.415C119.298 9.68387 119.08 9.29974 118.762 8.95829C118.108 8.19002 117.195 7.80589 116.022 7.80589C114.983 7.80589 114.111 8.20709 113.407 9.0095C112.72 9.81191 112.377 10.8619 112.377 12.1594C112.377 13.4569 112.72 14.5069 113.407 15.3093C114.111 16.1117 114.983 16.5129 116.022 16.5129C117.228 16.5129 118.175 16.1288 118.862 15.3605C119.164 15.019 119.39 14.6349 119.541 14.2081H122.181C121.963 15.0788 121.577 15.8727 121.024 16.5897C119.751 18.1604 118.083 18.9457 116.022 18.9457C114.195 18.9457 112.67 18.3055 111.446 17.0251Z" fill="#202020"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
3
src/icons/logo.svg
Normal file
3
src/icons/logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21611 1.09424C7.96286 0.347493 9.17358 0.347492 9.92033 1.09424L10.9714 2.14531C11.511 2.68494 12.386 2.68494 12.9256 2.1453L13.9767 1.09424C14.7234 0.347492 15.9341 0.347492 16.6809 1.09424L23.3374 7.75078C24.0842 8.49753 24.0842 9.70825 23.3374 10.455L22.2863 11.5061C21.7467 12.0457 21.7467 12.9206 22.2863 13.4603L23.3374 14.5113C24.0842 15.2581 24.0842 16.4688 23.3374 17.2155L16.6809 23.8721C15.9341 24.6188 14.7234 24.6188 13.9767 23.8721L12.9256 22.821C12.386 22.2814 11.511 22.2814 10.9714 22.821L9.92033 23.8721C9.17358 24.6188 7.96286 24.6188 7.21611 23.8721L0.559574 17.2155C-0.187175 16.4688 -0.187175 15.2581 0.559574 14.5113L1.61064 13.4603C2.15027 12.9206 2.15027 12.0457 1.61064 11.5061L0.559574 10.455C-0.187176 9.70825 -0.187176 8.49753 0.559574 7.75078L7.21611 1.09424ZM10.9714 20.5548C11.511 21.0944 12.386 21.0944 12.9256 20.5548L20.0201 13.4603C20.5598 12.9206 20.5598 12.0457 20.0201 11.5061L12.9256 4.41153C12.386 3.87189 11.511 3.87189 10.9714 4.41153L3.87686 11.5061C3.33722 12.0457 3.33722 12.9206 3.87686 13.4603L10.9714 20.5548Z" fill="#202020"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
src/icons/paperclip.svg
Normal file
3
src/icons/paperclip.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6558 9.46967C16.7255 9.53932 16.7808 9.62204 16.8186 9.71309C16.8563 9.80413 16.8757 9.90173 16.8757 10.0003C16.8757 10.0989 16.8563 10.1964 16.8186 10.2875C16.7808 10.3785 16.7255 10.4613 16.6558 10.5309L8.96359 18.2184C7.97885 19.203 6.6433 19.7561 5.25076 19.756C3.85821 19.756 2.52274 19.2027 1.53812 18.2179C0.553503 17.2332 0.000400399 15.8977 0.000488292 14.5051C0.000576185 13.1126 0.553847 11.7771 1.53859 10.7925L10.8442 1.34998C11.5472 0.646202 12.5011 0.250528 13.4958 0.250001C14.4906 0.249473 15.4448 0.644135 16.1486 1.34717C16.8524 2.0502 17.248 3.00401 17.2486 3.99877C17.2491 4.99353 16.8544 5.94776 16.1514 6.65154L6.8439 16.094C6.42121 16.5167 5.84792 16.7542 5.25015 16.7542C4.65238 16.7542 4.07909 16.5167 3.6564 16.094C3.23371 15.6714 2.99625 15.0981 2.99625 14.5003C2.99625 13.9025 3.23371 13.3292 3.6564 12.9065L11.4658 4.97342C11.5342 4.90044 11.6165 4.84188 11.7079 4.8012C11.7993 4.76051 11.8979 4.73851 11.9979 4.7365C12.0979 4.73448 12.1973 4.7525 12.2902 4.78947C12.3832 4.82645 12.4678 4.88165 12.539 4.95181C12.6103 5.02198 12.6669 5.10569 12.7053 5.19804C12.7438 5.29038 12.7634 5.38948 12.7629 5.4895C12.7625 5.58953 12.7421 5.68846 12.7029 5.78048C12.6636 5.87249 12.6064 5.95573 12.5345 6.02529L4.72421 13.9669C4.65428 14.0362 4.5987 14.1187 4.56065 14.2096C4.52259 14.3004 4.5028 14.3979 4.50241 14.4964C4.50202 14.5949 4.52103 14.6925 4.55836 14.7836C4.5957 14.8748 4.65062 14.9577 4.71999 15.0276C4.78937 15.0976 4.87184 15.1531 4.96269 15.1912C5.05355 15.2293 5.15101 15.249 5.24951 15.2494C5.34801 15.2498 5.44563 15.2308 5.53678 15.1935C5.62794 15.1562 5.71085 15.1012 5.78078 15.0319L15.0873 5.59404C15.51 5.17222 15.7478 4.59977 15.7485 4.00261C15.7491 3.40545 15.5124 2.83251 15.0906 2.40982C14.6688 1.98713 14.0963 1.74932 13.4992 1.74871C12.902 1.74809 12.3291 1.98472 11.9064 2.40654L2.60265 11.8453C2.25411 12.1933 1.97753 12.6065 1.78869 13.0614C1.59985 13.5162 1.50246 14.0039 1.50207 14.4964C1.50167 14.9889 1.59829 15.4767 1.78641 15.9318C1.97452 16.387 2.25045 16.8007 2.59843 17.1492C2.94641 17.4977 3.35964 17.7743 3.81451 17.9632C4.26938 18.152 4.757 18.2494 5.24951 18.2498C5.74202 18.2502 6.22979 18.1536 6.68496 17.9654C7.14014 17.7773 7.5538 17.5014 7.90234 17.1534L15.5955 9.46592C15.7366 9.32587 15.9276 9.24759 16.1264 9.24829C16.3252 9.249 16.5156 9.32862 16.6558 9.46967Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1,3 +1,4 @@
|
||||
export const LS_TOKENS = {
|
||||
isMainMenuOpen: "isMainMenuOpen",
|
||||
userId: "userId",
|
||||
};
|
||||
|
||||
@@ -10,43 +10,112 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as InteractionPanelRouteImport } from './routes/interaction-panel'
|
||||
import { Route as AuthRouteImport } from './routes/auth'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as MailCodeMailCodeRouteImport } from './routes/mail-code/$mail-code'
|
||||
import { Route as AuthUsernameRouteImport } from './routes/auth/username'
|
||||
import { Route as AuthMailRouteImport } from './routes/auth/mail'
|
||||
import { Route as AuthCodeRouteImport } from './routes/auth/code'
|
||||
|
||||
const InteractionPanelRoute = InteractionPanelRouteImport.update({
|
||||
id: '/interaction-panel',
|
||||
path: '/interaction-panel',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/auth',
|
||||
path: '/auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const MailCodeMailCodeRoute = MailCodeMailCodeRouteImport.update({
|
||||
id: '/mail-code/$mail-code',
|
||||
path: '/mail-code/$mail-code',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthUsernameRoute = AuthUsernameRouteImport.update({
|
||||
id: '/username',
|
||||
path: '/username',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthMailRoute = AuthMailRouteImport.update({
|
||||
id: '/mail',
|
||||
path: '/mail',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthCodeRoute = AuthCodeRouteImport.update({
|
||||
id: '/code',
|
||||
path: '/code',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/auth': typeof AuthRouteWithChildren
|
||||
'/interaction-panel': typeof InteractionPanelRoute
|
||||
'/auth/code': typeof AuthCodeRoute
|
||||
'/auth/mail': typeof AuthMailRoute
|
||||
'/auth/username': typeof AuthUsernameRoute
|
||||
'/mail-code/$mail-code': typeof MailCodeMailCodeRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/auth': typeof AuthRouteWithChildren
|
||||
'/interaction-panel': typeof InteractionPanelRoute
|
||||
'/auth/code': typeof AuthCodeRoute
|
||||
'/auth/mail': typeof AuthMailRoute
|
||||
'/auth/username': typeof AuthUsernameRoute
|
||||
'/mail-code/$mail-code': typeof MailCodeMailCodeRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/auth': typeof AuthRouteWithChildren
|
||||
'/interaction-panel': typeof InteractionPanelRoute
|
||||
'/auth/code': typeof AuthCodeRoute
|
||||
'/auth/mail': typeof AuthMailRoute
|
||||
'/auth/username': typeof AuthUsernameRoute
|
||||
'/mail-code/$mail-code': typeof MailCodeMailCodeRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/interaction-panel'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/auth'
|
||||
| '/interaction-panel'
|
||||
| '/auth/code'
|
||||
| '/auth/mail'
|
||||
| '/auth/username'
|
||||
| '/mail-code/$mail-code'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/interaction-panel'
|
||||
id: '__root__' | '/' | '/interaction-panel'
|
||||
to:
|
||||
| '/'
|
||||
| '/auth'
|
||||
| '/interaction-panel'
|
||||
| '/auth/code'
|
||||
| '/auth/mail'
|
||||
| '/auth/username'
|
||||
| '/mail-code/$mail-code'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/auth'
|
||||
| '/interaction-panel'
|
||||
| '/auth/code'
|
||||
| '/auth/mail'
|
||||
| '/auth/username'
|
||||
| '/mail-code/$mail-code'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AuthRoute: typeof AuthRouteWithChildren
|
||||
InteractionPanelRoute: typeof InteractionPanelRoute
|
||||
MailCodeMailCodeRoute: typeof MailCodeMailCodeRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -58,6 +127,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof InteractionPanelRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/auth': {
|
||||
id: '/auth'
|
||||
path: '/auth'
|
||||
fullPath: '/auth'
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -65,12 +141,56 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/mail-code/$mail-code': {
|
||||
id: '/mail-code/$mail-code'
|
||||
path: '/mail-code/$mail-code'
|
||||
fullPath: '/mail-code/$mail-code'
|
||||
preLoaderRoute: typeof MailCodeMailCodeRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/auth/username': {
|
||||
id: '/auth/username'
|
||||
path: '/username'
|
||||
fullPath: '/auth/username'
|
||||
preLoaderRoute: typeof AuthUsernameRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/auth/mail': {
|
||||
id: '/auth/mail'
|
||||
path: '/mail'
|
||||
fullPath: '/auth/mail'
|
||||
preLoaderRoute: typeof AuthMailRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/auth/code': {
|
||||
id: '/auth/code'
|
||||
path: '/code'
|
||||
fullPath: '/auth/code'
|
||||
preLoaderRoute: typeof AuthCodeRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthRouteChildren {
|
||||
AuthCodeRoute: typeof AuthCodeRoute
|
||||
AuthMailRoute: typeof AuthMailRoute
|
||||
AuthUsernameRoute: typeof AuthUsernameRoute
|
||||
}
|
||||
|
||||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
AuthCodeRoute: AuthCodeRoute,
|
||||
AuthMailRoute: AuthMailRoute,
|
||||
AuthUsernameRoute: AuthUsernameRoute,
|
||||
}
|
||||
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AuthRoute: AuthRouteWithChildren,
|
||||
InteractionPanelRoute: InteractionPanelRoute,
|
||||
MailCodeMailCodeRoute: MailCodeMailCodeRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
// src/routes/__root.tsx
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { MainMenu } from "@/components/common/main-menu";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { MainMenu } from "@/components/main-menu";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootLayout,
|
||||
});
|
||||
|
||||
function RootLayout() {
|
||||
const { userId } = useAccountStore();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<MainMenu />
|
||||
{userId && <MainMenu />}
|
||||
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
|
||||
22
src/routes/auth.tsx
Normal file
22
src/routes/auth.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
|
||||
export const Route = createFileRoute("/auth")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="h-full pt-5">
|
||||
<Link
|
||||
to="/"
|
||||
viewTransition={{ types: ["warp"] }}
|
||||
className="px-10 h-[52px] flex items-center absolute"
|
||||
>
|
||||
<LogoWithText />
|
||||
</Link>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
171
src/routes/auth/code.tsx
Normal file
171
src/routes/auth/code.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { TextLink } from "@/components/ui/text-link";
|
||||
|
||||
export const Route = createFileRoute("/auth/code")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const [code, setCode] = useState<string[]>(Array(6).fill(""));
|
||||
|
||||
const [resendTimeout, setResendTimeout] = useState(() => {
|
||||
const savedTimeout = localStorage.getItem("resendTimeout");
|
||||
const savedTimestamp = localStorage.getItem("resendTimeoutTimestamp");
|
||||
|
||||
if (savedTimeout && savedTimestamp) {
|
||||
const secondsPassed = Math.floor(
|
||||
(Date.now() - Number(savedTimestamp)) / 1000,
|
||||
);
|
||||
const remainingTime = Math.max(0, Number(savedTimeout) - secondsPassed);
|
||||
return remainingTime > 0 ? Math.floor(remainingTime) : 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.getElementById("code-input-0")?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (resendTimeout <= 0) {
|
||||
localStorage.removeItem("resendTimeout");
|
||||
localStorage.removeItem("resendTimeoutTimestamp");
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("resendTimeout", resendTimeout.toString());
|
||||
localStorage.setItem("resendTimeoutTimestamp", Date.now().toString());
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setResendTimeout(resendTimeout - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [resendTimeout]);
|
||||
|
||||
const handleChange = (index: number, value: string) => {
|
||||
if (!/^\d?$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.getElementById(`code-input-${index + 1}`);
|
||||
nextInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (
|
||||
index: number,
|
||||
e: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (e.key === "Backspace") {
|
||||
const newCode = [...code];
|
||||
|
||||
if (code[index] === "") {
|
||||
if (index > 0) {
|
||||
newCode[index - 1] = "";
|
||||
setCode(newCode);
|
||||
document.getElementById(`code-input-${index - 1}`)?.focus();
|
||||
}
|
||||
} else {
|
||||
newCode[index] = "";
|
||||
setCode(newCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pasted = e.clipboardData
|
||||
.getData("Text")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (!pasted) return;
|
||||
|
||||
const newCode = [...code];
|
||||
for (let i = 0; i < pasted.length; i++) {
|
||||
newCode[i] = pasted[i];
|
||||
}
|
||||
setCode(newCode);
|
||||
|
||||
const nextIndex = pasted.length < 6 ? pasted.length : 5;
|
||||
document.getElementById(`code-input-${nextIndex}`)?.focus();
|
||||
};
|
||||
|
||||
const handleResendClick = () => {
|
||||
setResendTimeout(60);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%]">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
<p className="text-text-light-200 whitespace-nowrap mt-4">
|
||||
We’ve sent a code to{" "}
|
||||
<span className="text-text-light-900">email@example.com </span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10">
|
||||
<div className="flex space-x-2">
|
||||
{code.map((digit, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
id={`code-input-${i}`}
|
||||
className="w-10 h-10 p-0 text-center text-lg"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="\d*"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onPaste={handlePaste}
|
||||
onChange={(e) => handleChange(i, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(i, e)}
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={code.some((c) => c === "")}
|
||||
onClick={() => {
|
||||
const fullCode = code.join("");
|
||||
console.log("Confirm code:", fullCode);
|
||||
navigate({
|
||||
to: "/auth/username",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
<div className="text-[14px] w-[286px] whitespace-nowrap">
|
||||
<span className="tracking-[-2%] text-text-light-200">
|
||||
Didn't receive a code?
|
||||
</span>
|
||||
{resendTimeout > 0 ? (
|
||||
<span className="font-[500] ml-4">
|
||||
Next resend in {resendTimeout}s
|
||||
</span>
|
||||
) : (
|
||||
<TextLink className="font-[500] ml-4" onClick={handleResendClick}>
|
||||
[ Click to resend ]
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
src/routes/auth/mail.tsx
Normal file
54
src/routes/auth/mail.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/auth/mail")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const [email, setEmail] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (email.trim() === "") return;
|
||||
navigate({
|
||||
to: "/auth/code",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%] ">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 w-[300px]">
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="Email@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={email.trim() === ""}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Send code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/routes/auth/username.tsx
Normal file
66
src/routes/auth/username.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/auth/username")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const [username, setUsername] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (username.trim() === "") return;
|
||||
navigate({
|
||||
to: "/",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%] ">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
<p className="text-text-light-200 whitespace-nowrap mt-4">
|
||||
Create a username
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 w-[300px]">
|
||||
<Input
|
||||
label="Username"
|
||||
placeholder="Pink Axolotl"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={username.trim() === ""}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="text-[18px] w-full"
|
||||
// onClick={handleSubmit}
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { TextLink } from "@/components/ui/text-link";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Chat } from "@/components/interaction-panel/chat";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: App,
|
||||
@@ -7,8 +9,16 @@ export const Route = createFileRoute("/")({
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<TextLink>Lable</TextLink>
|
||||
<div className="h-full pt-5">
|
||||
<header className="px-10 h-[52px] flex items-center justify-between absolute w-full">
|
||||
<LogoWithText />
|
||||
<Link to="/auth/mail" viewTransition={{ types: ["warp"] }}>
|
||||
<Button className=" text-[18px] w-[106px] cursor-pointer">
|
||||
Sign in
|
||||
</Button>
|
||||
</Link>
|
||||
</header>
|
||||
<Chat />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
193
src/routes/mail-code/$mail-code.tsx
Normal file
193
src/routes/mail-code/$mail-code.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { TextLink } from "@/components/ui/text-link";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
|
||||
export const Route = createFileRoute("/mail-code/$mail-code")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const { "mail-code": codeFromUrl } = Route.useParams();
|
||||
|
||||
const [code, setCode] = useState<string[]>(Array(6).fill(""));
|
||||
const [isAutoSubmitted, setIsAutoSubmitted] = useState(false);
|
||||
|
||||
const [resendTimeout, setResendTimeout] = useState(() => {
|
||||
const savedTimeout = localStorage.getItem("resendTimeout");
|
||||
const savedTimestamp = localStorage.getItem("resendTimeoutTimestamp");
|
||||
|
||||
if (savedTimeout && savedTimestamp) {
|
||||
const secondsPassed = Math.floor(
|
||||
(Date.now() - Number(savedTimestamp)) / 1000,
|
||||
);
|
||||
const remainingTime = Math.max(0, Number(savedTimeout) - secondsPassed);
|
||||
return remainingTime > 0 ? Math.floor(remainingTime) : 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (codeFromUrl && /^\d{6}$/.test(codeFromUrl)) {
|
||||
const codeArray = codeFromUrl.split("").slice(0, 6);
|
||||
setCode(codeArray);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
handleConfirm(codeArray.join(""));
|
||||
}, 800);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [codeFromUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resendTimeout <= 0) {
|
||||
localStorage.removeItem("resendTimeout");
|
||||
localStorage.removeItem("resendTimeoutTimestamp");
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("resendTimeout", resendTimeout.toString());
|
||||
localStorage.setItem("resendTimeoutTimestamp", Date.now().toString());
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setResendTimeout(resendTimeout - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [resendTimeout]);
|
||||
|
||||
const handleChange = (index: number, value: string) => {
|
||||
if (!/^\d?$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.getElementById(`code-input-${index + 1}`);
|
||||
nextInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (
|
||||
index: number,
|
||||
e: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (e.key === "Backspace") {
|
||||
const newCode = [...code];
|
||||
|
||||
if (code[index] === "") {
|
||||
if (index > 0) {
|
||||
newCode[index - 1] = "";
|
||||
setCode(newCode);
|
||||
document.getElementById(`code-input-${index - 1}`)?.focus();
|
||||
}
|
||||
} else {
|
||||
newCode[index] = "";
|
||||
setCode(newCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pasted = e.clipboardData
|
||||
.getData("Text")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (!pasted) return;
|
||||
|
||||
const newCode = [...code];
|
||||
for (let i = 0; i < pasted.length; i++) {
|
||||
newCode[i] = pasted[i];
|
||||
}
|
||||
setCode(newCode);
|
||||
|
||||
const nextIndex = pasted.length < 6 ? pasted.length : 5;
|
||||
document.getElementById(`code-input-${nextIndex}`)?.focus();
|
||||
};
|
||||
|
||||
const handleResendClick = () => {
|
||||
setResendTimeout(60);
|
||||
};
|
||||
|
||||
const handleConfirm = (fullCode: string) => {
|
||||
navigate({
|
||||
to: "/auth/username",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full pt-5">
|
||||
<Link
|
||||
to="/"
|
||||
viewTransition={{ types: ["warp"] }}
|
||||
className="px-10 h-[52px] flex items-center absolute"
|
||||
>
|
||||
<LogoWithText />
|
||||
</Link>
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%]">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10">
|
||||
<div className="flex space-x-2">
|
||||
{code.map((digit, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
id={`code-input-${i}`}
|
||||
className="w-10 h-10 p-0 text-center text-lg"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="\d*"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onPaste={handlePaste}
|
||||
onChange={(e) => handleChange(i, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(i, e)}
|
||||
autoComplete="one-time-code"
|
||||
readOnly={isAutoSubmitted}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={code.some((c) => c === "") || isAutoSubmitted}
|
||||
onClick={() => handleConfirm(code.join(""))}
|
||||
>
|
||||
{isAutoSubmitted ? "Processing..." : "Confirm"}
|
||||
</Button>
|
||||
|
||||
<div className="text-[14px] w-[286px] whitespace-nowrap">
|
||||
<span className="tracking-[-2%] text-text-light-200">
|
||||
Didn't receive a code?
|
||||
</span>
|
||||
{resendTimeout > 0 ? (
|
||||
<span className="font-[500] ml-4">
|
||||
Next resend in {resendTimeout}s
|
||||
</span>
|
||||
) : (
|
||||
<TextLink
|
||||
className="font-[500] ml-4"
|
||||
onClick={handleResendClick}
|
||||
>
|
||||
[ Click to resend ]
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { LS_TOKENS } from "@/lib/constants";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export class AccountStore {
|
||||
userId?: string;
|
||||
|
||||
isMainMenuOpen: boolean;
|
||||
isSandboxOpen: boolean;
|
||||
|
||||
@@ -12,6 +14,9 @@ export class AccountStore {
|
||||
this.isMainMenuOpen = savedState ? savedState === "true" : false;
|
||||
|
||||
this.isSandboxOpen = false;
|
||||
|
||||
const savedUserId = localStorage.getItem(LS_TOKENS.userId);
|
||||
this.userId = savedUserId ? savedUserId : undefined;
|
||||
}
|
||||
|
||||
setIsMainMenuOpen(value: boolean) {
|
||||
@@ -22,4 +27,9 @@ export class AccountStore {
|
||||
setIsSandboxOpen(value: boolean) {
|
||||
this.isSandboxOpen = value;
|
||||
}
|
||||
|
||||
setUserId(value: string) {
|
||||
this.userId = value;
|
||||
localStorage.setItem(LS_TOKENS.userId, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
@theme {
|
||||
--font-work: "work-sans", "sans-serif";
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-bg-dark:#212121;
|
||||
--color-bg-light:#FFFFFF;
|
||||
--color-brand-gray:#201E1E;
|
||||
|
||||
--color-text-light-900:#0A0A0A;
|
||||
--color-text-light-500:#939393;
|
||||
@@ -20,6 +27,7 @@
|
||||
--color-fill-700:#363636;
|
||||
--color-fill-400:#939393;
|
||||
--color-fill-300:#C0C0C0;
|
||||
--color-fill-200:#D6D6D6;
|
||||
--color-fill-150:#E8E8E8;
|
||||
--color-fill-100:#F5F5F5;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user