diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..ace22fd --- /dev/null +++ b/app/(auth)/layout.tsx @@ -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 ( +
+
+ +

OpenStock

+ + +
+ {children} +
+
+
+
+
+ “For me, OpenStock isn’t just another stock app. It’s about giving people clarity and control in the market, without barriers or subscriptions.” +
+
+
+ - Ravi Pratap Singh (@ravixalgorithm) +

Founder @opendevsociety

+
+
+ {[1,2,3,4,5].map((star) => ( + star + ))} +
+
+
+
+ Dashboard Preview +
+
+ +
+ ) +} +export default Layout diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx new file mode 100644 index 0000000..a07693e --- /dev/null +++ b/app/(auth)/sign-in/page.tsx @@ -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({ + defaultValues: { + email: '', + password: '', + }, + mode: 'onBlur' + }); + const onSubmit = async(data: SignInFormData) => { + try{ + console.log(data) + } catch(e){ + console.log(e); + } + } + return ( + <> +

Sign In

+ +
+ + + + + + + + + + + + + + + ) +} +export default SignIn diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx new file mode 100644 index 0000000..76f3084 --- /dev/null +++ b/app/(auth)/sign-up/page.tsx @@ -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({ + 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 ( + <> +

Sign Up & Personalize

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + ) +} +export default SignUp diff --git a/components/OpenDevSocietyBranding.tsx b/components/OpenDevSocietyBranding.tsx new file mode 100644 index 0000000..2d1d836 --- /dev/null +++ b/components/OpenDevSocietyBranding.tsx @@ -0,0 +1,194 @@ +import React from "react"; + +// SVG version of your logo (image 2) +// Replace with real SVG for sharpest results; this is an inline approximation +const ODSLogoSVG: React.FC<{ size?: number }> = ({ size = 26 }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +type OpenDevSocietyBrandingProps = { + text?: string; // e.g. "Designed by" + name?: string; // e.g. "Open Dev Society" + style?: React.CSSProperties; + className?: string; + logoSize?: number; + textColor?: string; + outerStyle?: React.CSSProperties; // NEW: outer style for container + outerClassName?: string; +}; + +export const OpenDevSocietyBranding: React.FC = ({ + text = "Initiative by", + name = "Open Dev Society", + style = {}, + className = "border-2 border-gray-300 px-3 py-0.5 rounded-lg", + logoSize = 40, + textColor = "#fff", + outerStyle = {}, + outerClassName = "", + }) => ( +
+
+ {text} + + {name} +
+
+); + +export default OpenDevSocietyBranding; \ No newline at end of file diff --git a/components/forms/CountrySelectField.tsx b/components/forms/CountrySelectField.tsx new file mode 100644 index 0000000..5677b1b --- /dev/null +++ b/components/forms/CountrySelectField.tsx @@ -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; + 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 ( + + + + + + + + + No country found. + + + + {countries.map((country) => ( + { + onChange(country.value); + setOpen(false); + }} + className='country-select-item' + > + + + {getFlagEmoji(country.value)} + {country.label} + + + ))} + + + + + + ); +}; + +export const CountrySelectField = ({ + name, + label, + control, + error, + required = false, + }: CountrySelectProps) => { + return ( +
+ + ( + + )} + /> + {error &&

{error.message}

} +

+ Helps us show market data and news relevant to you. +

+
+ ); +}; \ No newline at end of file diff --git a/components/forms/FooterLink.tsx b/components/forms/FooterLink.tsx new file mode 100644 index 0000000..d84946a --- /dev/null +++ b/components/forms/FooterLink.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import Link from "next/link"; + +const FooterLink = ({text, linkText, href}: FooterLinkProps) => { + return ( +
+

+ {text}{` `} + + {linkText} + +

+
+ ) +} +export default FooterLink diff --git a/components/forms/InputField.tsx b/components/forms/InputField.tsx new file mode 100644 index 0000000..245dedf --- /dev/null +++ b/components/forms/InputField.tsx @@ -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 ( +
+ + + {error &&

{error.message}

} +
+ ) +} +export default InputField diff --git a/components/forms/SelectField.tsx b/components/forms/SelectField.tsx new file mode 100644 index 0000000..c6c1c80 --- /dev/null +++ b/components/forms/SelectField.tsx @@ -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 ( +
+ + + ( + + )} + /> +
+ ) +} +export default SelectField diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..8cb4ca7 --- /dev/null +++ b/components/ui/command.tsx @@ -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) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/components/ui/dialog.tsx @@ -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) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/components/ui/label.tsx @@ -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) { + return ( + + ) +} + +export { Label } diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 0000000..01e468b --- /dev/null +++ b/components/ui/popover.tsx @@ -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) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..dcbbc0c --- /dev/null +++ b/components/ui/select.tsx @@ -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) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/lib/constants.ts b/lib/constants.ts index 66e51f4..06ff8d4 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -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' }, ], diff --git a/package-lock.json b/package-lock.json index bc8c456..9340050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9ec2d67..671a34e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/assets/icons/odsLogo.svg b/public/assets/icons/odsLogo.svg new file mode 100644 index 0000000..0a41f9b --- /dev/null +++ b/public/assets/icons/odsLogo.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/dashboard.png b/public/assets/images/dashboard.png index 85e63c7..ebe44a8 100644 Binary files a/public/assets/images/dashboard.png and b/public/assets/images/dashboard.png differ diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..b01836b --- /dev/null +++ b/types/global.d.ts @@ -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 {}; \ No newline at end of file