implemented auth ui and layout

This commit is contained in:
Mr. Algorithm 2025-10-02 23:16:08 +05:30
parent 8411bea110
commit 690a644af1
20 changed files with 1882 additions and 1 deletions

42
app/(auth)/layout.tsx Normal file
View File

@ -0,0 +1,42 @@
import Link from "next/link";
import React from "react";
import Image from "next/image";
const Layout = ({ children }: { children : React.ReactNode }) => {
return (
<main className="auth-layout">
<section className="auth-left-section scrollbar-hide-default">
<Link href="/" className="auth-logo">
<h2 className="text-3xl font-bold text-white">OpenStock</h2>
</Link>
<div className="pb-6 lg:pb-8 flex-1">
{children}
</div>
</section>
<section className="auth-right-section">
<div className="z-10 relative lg:mt-4 lg:mb-16">
<blockquote className="auth-blockquote">
For me, OpenStock isnt just another stock app. Its about giving people clarity and control in the market, without barriers or subscriptions.
</blockquote>
<div className="flex items-center justify-between">
<div>
<cite className="auth-testimonial-author">- Ravi Pratap Singh (@ravixalgorithm)</cite>
<p className="max-md:text-xs text-gray-500">Founder @opendevsociety</p>
</div>
<div className="flex items-center gap-0.5">
{[1,2,3,4,5].map((star) => (
<Image src="/assets/icons/star.svg" alt="star" key={star} width={20} height={20} className="w-4 h-4"/>
))}
</div>
</div>
</div>
<div className="flex-1 relative">
<Image src="/assets/images/dashboard.png" alt="Dashboard Preview" width={1440} height={1150} className="auth-dashboard-preview absolute top-0" />
</div>
</section>
</main>
)
}
export default Layout

View File

@ -0,0 +1,71 @@
'use client';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import InputField from '@/components/forms/InputField';
import FooterLink from '@/components/forms/FooterLink';
import OpenDevSocietyBranding from "@/components/OpenDevSocietyBranding";
import React from "react";
const SignIn = () => {
const {
register,
handleSubmit,
formState: {errors, isSubmitting},
} = useForm<SignInFormData>({
defaultValues: {
email: '',
password: '',
},
mode: 'onBlur'
});
const onSubmit = async(data: SignInFormData) => {
try{
console.log(data)
} catch(e){
console.log(e);
}
}
return (
<>
<h1 className="form-title">Sign In</h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<InputField
name="email"
label="Email"
placeholder="Your email"
register={register}
error={errors.email}
validation={{required: 'Email is required', pattern: /^\w+@\w+$/, message: 'Email is required' }}
/>
<InputField
name="password"
label="Password"
placeholder="Enter password"
type="password"
register={register}
error={errors.password}
validation={{required: 'Password is required', minLength: 8}}
/>
<Button type="submit" disabled={isSubmitting} className="yellow-btn w-full mt-5">
{isSubmitting ? 'Signing In...' : 'Sign In'}
</Button>
<FooterLink
text="Don't have an account?"
linkText="Sign up"
href="/sign-up"
/>
</form>
<OpenDevSocietyBranding outerClassName="mt-10 flex justify-center"/>
</>
)
}
export default SignIn

124
app/(auth)/sign-up/page.tsx Normal file
View File

@ -0,0 +1,124 @@
'use client';
import React from 'react'
import {useForm} from "react-hook-form";
import {Button} from "@/components/ui/button";
import InputField from "@/components/forms/InputField";
import SelectField from "@/components/forms/SelectField";
import {INVESTMENT_GOALS, PREFERRED_INDUSTRIES, RISK_TOLERANCE_OPTIONS} from "@/lib/constants";
import {CountrySelectField} from "@/components/forms/CountrySelectField";
import FooterLink from "@/components/forms/FooterLink";
import Link from "next/link";
import OpenDevSocietyBranding from "@/components/OpenDevSocietyBranding";
const SignUp = () => {
const {
register,
handleSubmit,
control,
formState: {errors, isSubmitting},
} = useForm<SignUpFormData>({
defaultValues: {
fullName: '',
email: '',
password: '',
country: 'IN',
investmentGoals: 'Growth',
riskTolerance: 'Medium',
preferredIndustry: 'Technology'
},
mode: 'onBlur'
});
const onSubmit = async(data: SignUpFormData) => {
try{
console.log(data)
} catch(e){
console.log(e);
}
}
return (
<>
<h1 className="form-title">Sign Up & Personalize</h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<InputField
name="fullName"
label="Full Name"
placeholder="Your Name"
register={register}
error={errors.fullName}
validation={{required: 'Full name is required', minLength: 2 }}
/>
<InputField
name="email"
label="Email"
placeholder="Your email"
register={register}
error={errors.email}
validation={{required: 'Email is required', pattern: /^\w+@\w+$/, message: 'Email is required' }}
/>
<InputField
name="password"
label="Password"
placeholder="Enter password"
type="password"
register={register}
error={errors.password}
validation={{required: 'Password is required', minLength: 8}}
/>
<CountrySelectField
name="country"
label="Country"
control={control}
error={errors.country}
required
/>
<SelectField
name="investmentGoals"
label="Investment Goals"
placeholder="Select your investment goal"
options={INVESTMENT_GOALS}
control={control}
error={errors.investmentGoals}
/>
<SelectField
name="riskTolerance"
label="Risk Tolerance"
placeholder="Select your risk level"
options={RISK_TOLERANCE_OPTIONS}
control={control}
error={errors.riskTolerance}
/>
<SelectField
name="preferredIndustry"
label="Preferred Industry"
placeholder="Select your investment goal"
options={PREFERRED_INDUSTRIES}
control={control}
error={errors.preferredIndustry}
/>
<Button type="submit" disabled={isSubmitting} className="yellow-btn w-full mt-5">
{isSubmitting ? 'Creating account...' : 'Start Your Investing Journey'}
</Button>
<FooterLink
text="Already have an account?"
linkText="Sign in"
href="/sign-in"
/>
</form>
<OpenDevSocietyBranding outerClassName="mt-10 flex justify-center"/>
</>
)
}
export default SignUp

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,146 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { useState } from 'react';
import { Control, Controller, FieldError } from 'react-hook-form';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import countryList from 'react-select-country-list';
type CountrySelectProps = {
name: string;
label: string;
control: Control<any>;
error?: FieldError;
required?: boolean;
};
const CountrySelect = ({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) => {
const [open, setOpen] = useState(false);
// Get country options with flags
const countries = countryList().getData();
// Helper function to get flag emoji
const getFlagEmoji = (countryCode: string) => {
const codePoints = countryCode
.toUpperCase()
.split('')
.map((char) => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className='country-select-trigger'
>
{value ? (
<span className='flex items-center gap-2'>
<span>{getFlagEmoji(value)}</span>
<span>{countries.find((c) => c.value === value)?.label}</span>
</span>
) : (
'Select your country...'
)}
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent
className='w-full p-0 bg-gray-800 border-gray-600'
align='start'
>
<Command className='bg-gray-800 border-gray-600'>
<CommandInput
placeholder='Search countries...'
className='country-select-input'
/>
<CommandEmpty className='country-select-empty'>
No country found.
</CommandEmpty>
<CommandList className='max-h-60 bg-gray-800 scrollbar-hide-default'>
<CommandGroup className='bg-gray-800'>
{countries.map((country) => (
<CommandItem
key={country.value}
value={`${country.label} ${country.value}`}
onSelect={() => {
onChange(country.value);
setOpen(false);
}}
className='country-select-item'
>
<Check
className={cn(
'mr-2 h-4 w-4 text-yellow-500',
value === country.value ? 'opacity-100' : 'opacity-0'
)}
/>
<span className='flex items-center gap-2'>
<span>{getFlagEmoji(country.value)}</span>
<span>{country.label}</span>
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export const CountrySelectField = ({
name,
label,
control,
error,
required = false,
}: CountrySelectProps) => {
return (
<div className='space-y-2'>
<Label htmlFor={name} className='form-label'>
{label}
</Label>
<Controller
name={name}
control={control}
rules={{
required: required ? `Please select ${label.toLowerCase()}` : false,
}}
render={({ field }) => (
<CountrySelect value={field.value} onChange={field.onChange} />
)}
/>
{error && <p className='text-sm text-red-500'>{error.message}</p>}
<p className='text-xs text-gray-500'>
Helps us show market data and news relevant to you.
</p>
</div>
);
};

View File

@ -0,0 +1,16 @@
import React from 'react'
import Link from "next/link";
const FooterLink = ({text, linkText, href}: FooterLinkProps) => {
return (
<div className="text-center pt-4">
<p className="text-sm text-gray-500">
{text}{` `}
<Link href={href} className="footer-link">
{linkText}
</Link>
</p>
</div>
)
}
export default FooterLink

View File

@ -0,0 +1,25 @@
import React from 'react'
import {Label} from "@/components/ui/label";
import {Input} from "@/components/ui/input";
import {cn} from "@/lib/utils";
const InputField = ({name, label, placeholder, type ="text", register, error, validation, disabled, value}: FormInputProps) => {
return (
<div className="space-y-2">
<Label htmlFor={name} className="form-label">
{label}
</Label>
<Input
type={type}
id={name}
placeholder={placeholder}
disabled={disabled}
value={value}
className={cn('form-input', {'opacity-50 cursor-not-allowed': disabled})}
{...register(name, validation)}
/>
{error && <p className="text-red-500">{error.message}</p>}
</div>
)
}
export default InputField

View File

@ -0,0 +1,42 @@
import React from 'react'
import {Label} from "@/components/ui/label";
import {Controller} from "react-hook-form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const SelectField = ({name, label, placeholder, options, control, error, required = false}: SelectFieldProps) => {
return (
<div className="space-y-2">
<Label htmlFor={name}>{label}</Label>
<Controller
name={name}
control={control}
rules={{
required: required ? `Please select ${label.toLowerCase()}`:false,
}}
render={({field}) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="select-trigger">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-600 text-white">
{options.map((option) => (
<SelectItem key={option.value} value={option.value} className="focus:bg-gray-600 focus: text-white">
{option.label}
</SelectItem>
))}
</SelectContent>
{error && <p className="text-red-600">{error.message}</p>}
</Select>
)}
/>
</div>
)
}
export default SelectField

184
components/ui/command.tsx Normal file
View File

@ -0,0 +1,184 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

143
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

21
components/ui/input.tsx Normal file
View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

24
components/ui/label.tsx Normal file
View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

48
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

185
components/ui/select.tsx Normal file
View File

@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -71,7 +71,7 @@ export const MARKET_OVERVIEW_WIDGET_CONFIG = {
{ s: 'NASDAQ:AAPL', d: 'Apple' },
{ s: 'NASDAQ:GOOGL', d: 'Alphabet' },
{ s: 'NASDAQ:MSFT', d: 'Microsoft' },
{ s: 'NASDAQ:FB', d: 'Meta Platforms' },
{ s: 'NASDAQ:META', d: 'Meta Platforms' },
{ s: 'NYSE:ORCL', d: 'Oracle Corp' },
{ s: 'NASDAQ:INTC', d: 'Intel Corp' },
],

256
package-lock.json generated
View File

@ -9,14 +9,23 @@
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"country-data-list": "^1.5.5",
"lucide-react": "^0.544.0",
"next": "15.5.4",
"react": "19.1.0",
"react-circle-flags": "^0.0.23",
"react-dom": "19.1.0",
"react-hook-form": "^7.63.0",
"react-select-country-list": "^2.2.3",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
@ -25,6 +34,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-select-country-list": "^2.2.3",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"tailwindcss": "^4",
@ -1005,6 +1015,12 @@
"node": ">=12.4.0"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@ -1117,6 +1133,42 @@
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@ -1246,6 +1298,29 @@
}
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
@ -1286,6 +1361,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
@ -1420,6 +1532,49 @@
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@ -1541,6 +1696,21 @@
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
@ -1577,6 +1747,29 @@
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
@ -1946,6 +2139,13 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-select-country-list": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@types/react-select-country-list/-/react-select-country-list-2.2.3.tgz",
"integrity": "sha512-nffcYOwuun+5B0EWqubK+amHpPdK9Xj20xkLYNqYrzmESd8FnpLwHsS79ClLAWA9y+icVA8gWPkbwBp1gpjSwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.44.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz",
@ -2968,6 +3168,22 @@
"node": ">=6"
}
},
"node_modules/cmdk": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-id": "^1.1.0",
"@radix-ui/react-primitive": "^2.0.2"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2995,6 +3211,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/country-data-list": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/country-data-list/-/country-data-list-1.5.5.tgz",
"integrity": "sha512-igoAbJvlD/foOq4+aB3t24Hndvb5y/DasXcrVOtJMZ0UFLgfYq/mPMcCcSjJAIykLcTWOHZehZgNUJyaKMz1iA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -5710,6 +5932,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-circle-flags": {
"version": "0.0.23",
"resolved": "https://registry.npmjs.org/react-circle-flags/-/react-circle-flags-0.0.23.tgz",
"integrity": "sha512-J35vkTnG4Iz7cgitq/UIw4/1i8aUWEkz2Z0MQTTWycWOHKezuey5+3BPAsXNr78gYMdjSBtZWmSzQc0oEeK4oQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16.0.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@ -5723,6 +5957,22 @@
"react": "^19.1.0"
}
},
"node_modules/react-hook-form": {
"version": "7.63.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -5777,6 +6027,12 @@
}
}
},
"node_modules/react-select-country-list": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-select-country-list/-/react-select-country-list-2.2.3.tgz",
"integrity": "sha512-eRgXL613dVyJiE99yKDYLvSBKDxvIlhkmvO2DVIjdKVyUQq6kBqoMUV/2zuRIAsbRXgBGmKjeL1dxjf7zTfszg==",
"license": "MIT"
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",

View File

@ -10,14 +10,23 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"country-data-list": "^1.5.5",
"lucide-react": "^0.544.0",
"next": "15.5.4",
"react": "19.1.0",
"react-circle-flags": "^0.0.23",
"react-dom": "19.1.0",
"react-hook-form": "^7.63.0",
"react-select-country-list": "^2.2.3",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
@ -26,6 +35,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-select-country-list": "^2.2.3",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"tailwindcss": "^4",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 268 KiB

220
types/global.d.ts vendored Normal file
View File

@ -0,0 +1,220 @@
declare global {
type SignInFormData = {
email: string;
password: string;
};
type SignUpFormData = {
fullName: string;
email: string;
password: string;
country: string;
investmentGoals: string;
riskTolerance: string;
preferredIndustry: string;
};
type CountrySelectProps = {
name: string;
label: string;
control: Control;
error?: FieldError;
required?: boolean;
};
type FormInputProps = {
name: string;
label: string;
placeholder: string;
type?: string;
register: UseFormRegister;
error?: FieldError;
validation?: RegisterOptions;
disabled?: boolean;
value?: string;
};
type Option = {
value: string;
label: string;
};
type SelectFieldProps = {
name: string;
label: string;
placeholder: string;
options: readonly Option[];
control: Control;
error?: FieldError;
required?: boolean;
};
type FooterLinkProps = {
text: string;
linkText: string;
href: string;
};
type SearchCommandProps = {
renderAs?: 'button' | 'text';
label?: string;
initialStocks: StockWithWatchlistStatus[];
};
type WelcomeEmailData = {
email: string;
name: string;
intro: string;
};
type User = {
id: string;
name: string;
email: string;
};
type Stock = {
symbol: string;
name: string;
exchange: string;
type: string;
};
type StockWithWatchlistStatus = Stock & {
isInWatchlist: boolean;
};
type FinnhubSearchResult = {
symbol: string;
description: string;
displaySymbol?: string;
type: string;
};
type FinnhubSearchResponse = {
count: number;
result: FinnhubSearchResult[];
};
type StockDetailsPageProps = {
params: Promise<{
symbol: string;
}>;
};
type WatchlistButtonProps = {
symbol: string;
company: string;
isInWatchlist: boolean;
showTrashIcon?: boolean;
type?: 'button' | 'icon';
onWatchlistChange?: (symbol: string, isAdded: boolean) => void;
};
type QuoteData = {
c?: number;
dp?: number;
};
type ProfileData = {
name?: string;
marketCapitalization?: number;
};
type FinancialsData = {
metric?: { [key: string]: number };
};
type SelectedStock = {
symbol: string;
company: string;
currentPrice?: number;
};
type WatchlistTableProps = {
watchlist: StockWithData[];
};
type StockWithData = {
userId: string;
symbol: string;
company: string;
addedAt: Date;
currentPrice?: number;
changePercent?: number;
priceFormatted?: string;
changeFormatted?: string;
marketCap?: string;
peRatio?: string;
};
type AlertsListProps = {
alertData: Alert[] | undefined;
};
type MarketNewsArticle = {
id: number;
headline: string;
summary: string;
source: string;
url: string;
datetime: number;
category: string;
related: string;
image?: string;
};
type WatchlistNewsProps = {
news?: MarketNewsArticle[];
};
type SearchCommandProps = {
open?: boolean;
setOpen?: (open: boolean) => void;
renderAs?: 'button' | 'text';
buttonLabel?: string;
buttonVariant?: 'primary' | 'secondary';
className?: string;
};
type AlertData = {
symbol: string;
company: string;
alertName: string;
alertType: 'upper' | 'lower';
threshold: string;
};
type AlertModalProps = {
alertId?: string;
alertData?: AlertData;
action?: string;
open: boolean;
setOpen: (open: boolean) => void;
};
type RawNewsArticle = {
id: number;
headline?: string;
summary?: string;
source?: string;
url?: string;
datetime?: number;
image?: string;
category?: string;
related?: string;
};
type Alert = {
id: string;
symbol: string;
company: string;
alertName: string;
currentPrice: number;
alertType: 'upper' | 'lower';
threshold: number;
changePercent?: number;
};
}
export {};