mirror of
https://github.com/Open-Dev-Society/OpenStock.git
synced 2026-06-01 22:51:42 +08:00
implement auth logic
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import {headers} from "next/headers";
|
||||
import {redirect} from "next/navigation";
|
||||
import {auth} from "@/lib/better-auth/auth";
|
||||
|
||||
const Layout = ({ children }: { children : React.ReactNode }) => {
|
||||
const Layout = async ({ children }: { children : React.ReactNode }) => {
|
||||
|
||||
const session = await auth.api.getSession({headers: await headers()});
|
||||
|
||||
if (session?.user) redirect('/')
|
||||
return (
|
||||
<main className="auth-layout">
|
||||
<section className="auth-left-section scrollbar-hide-default">
|
||||
<Link href="/" className="auth-logo">
|
||||
<Link href="/" className="auth-logo flex items-center gap-2">
|
||||
<Image src="/assets/images/logo.png" alt="" width={30} height={30}/>
|
||||
<h2 className="text-3xl font-bold text-white">OpenStock</h2>
|
||||
</Link>
|
||||
|
||||
|
||||
@@ -4,68 +4,71 @@ 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 {signInWithEmail, signUpWithEmail} from "@/lib/actions/auth.actions";
|
||||
import {toast} from "sonner";
|
||||
import {signInEmail} from "better-auth/api";
|
||||
import {useRouter} from "next/navigation";
|
||||
import OpenDevSocietyBranding from "@/components/OpenDevSocietyBranding";
|
||||
import React from "react";
|
||||
|
||||
const SignIn = () => {
|
||||
const router = useRouter()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: {errors, isSubmitting},
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<SignInFormData>({
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
mode: 'onBlur'
|
||||
mode: 'onBlur',
|
||||
});
|
||||
const onSubmit = async(data: SignInFormData) => {
|
||||
try{
|
||||
console.log(data)
|
||||
} catch(e){
|
||||
console.log(e);
|
||||
|
||||
const onSubmit = async (data: SignInFormData) => {
|
||||
try {
|
||||
const result = await signInWithEmail(data);
|
||||
if(result.success) router.push('/');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error('Sign in failed', {
|
||||
description: e instanceof Error ? e.message : 'Failed to sign in.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="form-title">Sign In</h1>
|
||||
<h1 className="form-title">Welcome back</h1>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
|
||||
|
||||
<InputField
|
||||
name="email"
|
||||
label="Email"
|
||||
placeholder="Your email"
|
||||
placeholder="contact@jsmastery.com"
|
||||
register={register}
|
||||
error={errors.email}
|
||||
validation={{required: 'Email is required', pattern: /^\w+@\w+$/, message: 'Email is required' }}
|
||||
validation={{ required: 'Email is required', pattern: /^\w+@\w+\.\w+$/ }}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Enter password"
|
||||
placeholder="Enter your password"
|
||||
type="password"
|
||||
register={register}
|
||||
error={errors.password}
|
||||
validation={{required: 'Password is required', minLength: 8}}
|
||||
validation={{ required: 'Password is required', minLength: 8 }}
|
||||
/>
|
||||
|
||||
|
||||
<Button type="submit" disabled={isSubmitting} className="yellow-btn w-full mt-5">
|
||||
{isSubmitting ? 'Signing In...' : 'Sign In'}
|
||||
{isSubmitting ? 'Signing In' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
<FooterLink
|
||||
text="Don't have an account?"
|
||||
linkText="Sign up"
|
||||
href="/sign-up"
|
||||
/>
|
||||
<FooterLink text="Don't have an account?" linkText="Create an account" href="/sign-up" />
|
||||
<OpenDevSocietyBranding outerClassName="mt-10 flex justify-center"/>
|
||||
</form>
|
||||
|
||||
<OpenDevSocietyBranding outerClassName="mt-10 flex justify-center"/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default SignIn
|
||||
);
|
||||
};
|
||||
export default SignIn;
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react'
|
||||
import {useForm} from "react-hook-form";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import InputField from "@/components/forms/InputField";
|
||||
@@ -8,15 +7,19 @@ 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 {signUpWithEmail} from "@/lib/actions/auth.actions";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import OpenDevSocietyBranding from "@/components/OpenDevSocietyBranding";
|
||||
import React from "react";
|
||||
|
||||
const SignUp = () => {
|
||||
const router = useRouter()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: {errors, isSubmitting},
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<SignUpFormData>({
|
||||
defaultValues: {
|
||||
fullName: '',
|
||||
@@ -28,14 +31,20 @@ const SignUp = () => {
|
||||
preferredIndustry: 'Technology'
|
||||
},
|
||||
mode: 'onBlur'
|
||||
});
|
||||
const onSubmit = async(data: SignUpFormData) => {
|
||||
try{
|
||||
console.log(data)
|
||||
} catch(e){
|
||||
console.log(e);
|
||||
}, );
|
||||
|
||||
const onSubmit = async (data: SignUpFormData) => {
|
||||
try {
|
||||
const result = await signUpWithEmail(data);
|
||||
if(result.success) router.push('/');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error('Sign up failed', {
|
||||
description: e instanceof Error ? e.message : 'Failed to create an account.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="form-title">Sign Up & Personalize</h1>
|
||||
@@ -44,29 +53,29 @@ const SignUp = () => {
|
||||
<InputField
|
||||
name="fullName"
|
||||
label="Full Name"
|
||||
placeholder="Your Name"
|
||||
placeholder="Enter full name"
|
||||
register={register}
|
||||
error={errors.fullName}
|
||||
validation={{required: 'Full name is required', minLength: 2 }}
|
||||
validation={{ required: 'Full name is required', minLength: 2 }}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
name="email"
|
||||
label="Email"
|
||||
placeholder="Your email"
|
||||
placeholder="opendevsociety@cc.cc"
|
||||
register={register}
|
||||
error={errors.email}
|
||||
validation={{required: 'Email is required', pattern: /^\w+@\w+$/, message: 'Email is required' }}
|
||||
validation={{ required: 'Email name is required', pattern: /^\w+@\w+\.\w+$/, message: 'Email address is required' }}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Enter password"
|
||||
placeholder="Enter a strong password"
|
||||
type="password"
|
||||
register={register}
|
||||
error={errors.password}
|
||||
validation={{required: 'Password is required', minLength: 8}}
|
||||
validation={{ required: 'Password is required', minLength: 8 }}
|
||||
/>
|
||||
|
||||
<CountrySelectField
|
||||
@@ -84,6 +93,7 @@ const SignUp = () => {
|
||||
options={INVESTMENT_GOALS}
|
||||
control={control}
|
||||
error={errors.investmentGoals}
|
||||
required
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
@@ -93,32 +103,28 @@ const SignUp = () => {
|
||||
options={RISK_TOLERANCE_OPTIONS}
|
||||
control={control}
|
||||
error={errors.riskTolerance}
|
||||
required
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
name="preferredIndustry"
|
||||
label="Preferred Industry"
|
||||
placeholder="Select your investment goal"
|
||||
placeholder="Select your preferred industry"
|
||||
options={PREFERRED_INDUSTRIES}
|
||||
control={control}
|
||||
error={errors.preferredIndustry}
|
||||
required
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isSubmitting} className="yellow-btn w-full mt-5">
|
||||
{isSubmitting ? 'Creating account...' : 'Start Your Investing Journey'}
|
||||
{isSubmitting ? 'Creating Account' : 'Start Your Investing Journey'}
|
||||
</Button>
|
||||
|
||||
<FooterLink
|
||||
text="Already have an account?"
|
||||
linkText="Sign in"
|
||||
href="/sign-in"
|
||||
/>
|
||||
<FooterLink text="Already have an account?" linkText="Sign in" href="/sign-in" />
|
||||
|
||||
<OpenDevSocietyBranding outerClassName="mt-10 flex justify-center"/>
|
||||
</form>
|
||||
|
||||
|
||||
<OpenDevSocietyBranding outerClassName="mt-10 flex justify-center"/>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default SignUp
|
||||
export default SignUp;
|
||||
@@ -1,10 +1,22 @@
|
||||
import React from 'react'
|
||||
import Header from "../../components/Header";
|
||||
import Header from "@/components/Header";
|
||||
import {auth} from "@/lib/better-auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {redirect} from "next/navigation";
|
||||
|
||||
const Layout = async ({ children }: { children : React.ReactNode }) => {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
if(!session?.user) redirect('/sign-in');
|
||||
|
||||
const user = {
|
||||
id: session.user.id,
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
}
|
||||
|
||||
const Layout = ({ children }: { children : React.ReactNode }) => {
|
||||
return (
|
||||
<main className="min-h-screen text-gray-400">
|
||||
<Header/>
|
||||
<Header user={user} />
|
||||
|
||||
<div className="container py-10">
|
||||
{children}
|
||||
@@ -12,4 +24,4 @@ const Layout = ({ children }: { children : React.ReactNode }) => {
|
||||
</main>
|
||||
)
|
||||
}
|
||||
export default Layout
|
||||
export default Layout
|
||||
8
app/api/inngest/route.ts
Normal file
8
app/api/inngest/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {serve} from "inngest/next";
|
||||
import {inngest} from "@/lib/inngest/client";
|
||||
import {sendSignUpEmail} from "@/lib/inngest/functions";
|
||||
|
||||
export const {GET, POST, PUT } = serve({
|
||||
client: inngest,
|
||||
functions: [sendSignUpEmail],
|
||||
})
|
||||
@@ -146,7 +146,7 @@
|
||||
@apply mx-auto max-w-screen-2xl px-4 md:px-6 lg:px-8;
|
||||
}
|
||||
.yellow-btn {
|
||||
@apply h-12 cursor-pointer bg-gradient-to-b from-yellow-400 to-yellow-500 hover:from-yellow-500 hover:to-yellow-400 text-gray-950 font-medium text-base rounded-lg shadow-lg disabled:opacity-50;
|
||||
@apply h-12 cursor-pointer bg-gradient-to-b from-teal-400 to-teal-500 hover:from-teal-500 hover:to-teal-400 text-gray-800 font-medium text-base rounded-lg shadow-lg disabled:opacity-50;
|
||||
}
|
||||
.home-wrapper {
|
||||
@apply text-gray-400 flex-col gap-4 md:gap-10 items-center sm:items-start;
|
||||
@@ -188,13 +188,13 @@
|
||||
@apply text-sm font-medium text-gray-400;
|
||||
}
|
||||
.form-input {
|
||||
@apply h-12 px-3 py-3 text-white text-base placeholder:text-gray-500 border-gray-600 bg-gray-800 rounded-lg focus:!border-yellow-500 focus:ring-0;
|
||||
@apply h-12 px-3 py-3 text-white text-base placeholder:text-gray-600 border-gray-600 rounded-lg focus:!border-teal-500 focus:ring-0 ;
|
||||
}
|
||||
.select-trigger {
|
||||
@apply w-full !h-12 px-3 py-3 text-base border-gray-600 bg-gray-800 text-white rounded-lg focus:!border-yellow-500 focus:ring-0;
|
||||
@apply w-full !h-12 px-3 py-3 text-base border-gray-600 bg-gray-800 text-white rounded-lg focus:!border-teal-500 focus:ring-0;
|
||||
}
|
||||
.country-select-trigger {
|
||||
@apply h-12 px-3 py-3 text-base w-full justify-between font-normal border-gray-600 bg-gray-800 text-gray-400 rounded-lg focus:!border-yellow-500 focus:ring-0;
|
||||
@apply h-12 px-3 py-3 text-base w-full justify-between font-normal border-gray-600 bg-gray-800 text-gray-400 rounded-lg focus:!border-teal-500 focus:ring-0;
|
||||
}
|
||||
.country-select-input {
|
||||
@apply !bg-gray-800 text-gray-400 border-0 border-b border-gray-600 rounded-none focus:ring-0 placeholder:text-gray-500;
|
||||
@@ -206,13 +206,13 @@
|
||||
@apply text-white cursor-pointer px-3 py-2 rounded-sm bg-gray-800 hover:!bg-gray-600;
|
||||
}
|
||||
.footer-link {
|
||||
@apply text-gray-400 font-medium hover:text-yellow-400 hover:underline transition-colors;
|
||||
@apply text-gray-400 font-medium hover:text-teal-400 hover:underline transition-colors;
|
||||
}
|
||||
.search-text {
|
||||
@apply cursor-pointer hover:text-yellow-500;
|
||||
@apply cursor-pointer hover:text-teal-500;
|
||||
}
|
||||
.search-btn {
|
||||
@apply cursor-pointer px-4 py-2 w-fit flex items-center gap-2 text-sm md:text-base bg-yellow-500 hover:bg-yellow-500 text-black font-medium rounded;
|
||||
@apply cursor-pointer px-4 py-2 w-fit flex items-center gap-2 text-sm md:text-base bg-teal-500 hover:bg-teal-500 text-black font-medium rounded;
|
||||
}
|
||||
.search-dialog {
|
||||
@apply !bg-gray-800 lg:min-w-[800px] border-gray-600 fixed top-10 left-1/2 -translate-x-1/2 translate-y-10;
|
||||
@@ -254,7 +254,7 @@
|
||||
@apply w-full grid-cols-1 gap-6 xl:grid-cols-3 space-y-6 sm:space-y-8;
|
||||
}
|
||||
.watchlist-btn {
|
||||
@apply bg-yellow-500 text-base hover:bg-yellow-500 text-gray-900 w-full rounded h-11 font-semibold cursor-pointer;
|
||||
@apply bg-teal-500 text-base hover:bg-teal-500 text-gray-900 w-full rounded h-11 font-semibold cursor-pointer;
|
||||
}
|
||||
.watchlist-remove {
|
||||
@apply bg-red-500! hover:bg-red-500! text-gray-900!
|
||||
@@ -284,10 +284,10 @@
|
||||
@apply items-start gap-6 h-full flex-col w-full lg:col-span-1;
|
||||
}
|
||||
.watchlist-icon-btn {
|
||||
@apply w-fit cursor-pointer hover:bg-transparent! text-gray-400 hover:text-yellow-500;
|
||||
@apply w-fit cursor-pointer hover:bg-transparent! text-gray-400 hover:text-teal-500;
|
||||
}
|
||||
.watchlist-icon-added {
|
||||
@apply !text-yellow-500 hover:!text-yellow-600;
|
||||
@apply !text-teal-500 hover:!text-teal-600;
|
||||
}
|
||||
.watchlist-icon {
|
||||
@apply w-8 h-8 rounded-full flex items-center justify-center bg-gray-700/50;
|
||||
@@ -317,7 +317,7 @@
|
||||
@apply font-medium text-base
|
||||
}
|
||||
.add-alert {
|
||||
@apply flex text-sm items-center whitespace-nowrap gap-1.5 px-3 w-fit py-2 text-yellow-600 border border-yellow-600/20 rounded font-medium bg-transparent hover:bg-transparent cursor-pointer transition-colors;
|
||||
@apply flex text-sm items-center whitespace-nowrap gap-1.5 px-3 w-fit py-2 text-teal-600 border border-teal-600/20 rounded font-medium bg-transparent hover:bg-transparent cursor-pointer transition-colors;
|
||||
}
|
||||
.watchlist-news {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4;
|
||||
@@ -338,7 +338,7 @@
|
||||
@apply text-gray-400 flex-1 text-base leading-relaxed mb-3 line-clamp-3;
|
||||
}
|
||||
.news-cta {
|
||||
@apply text-sm align-bottom text-yellow-500 hover:text-gray-400;
|
||||
@apply text-sm align-bottom text-teal-500 hover:text-gray-400;
|
||||
}
|
||||
.alert-dialog {
|
||||
@apply bg-gray-800 border-gray-600 text-gray-400 max-w-md;
|
||||
@@ -356,7 +356,7 @@
|
||||
@apply p-4 rounded-lg bg-gray-700 border border-gray-600;
|
||||
}
|
||||
.alert-name {
|
||||
@apply mb-2 text-lg text-yellow-500 font-semibold;
|
||||
@apply mb-2 text-lg text-teal-500 font-semibold;
|
||||
}
|
||||
.alert-details {
|
||||
@apply flex border-b pb-3 items-center justify-between gap-3 mb-2;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import {Toaster} from "@/components/ui/sonner";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -24,11 +25,12 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<Toaster/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import NavItems from "@/components/NavItems";
|
||||
import UserDropdown from "@/components/UserDropdown";
|
||||
|
||||
const Header = () => {
|
||||
const Header = async ({ user }: { user: User }) => {
|
||||
return (
|
||||
<header className="sticky top-0 header">
|
||||
<div className="container header-wrapper">
|
||||
<Link href="/">
|
||||
<Link href="/" className="flex items-center justify-center gap-2">
|
||||
<Image src="/assets/images/logo.png" alt="" width={30} height={30}/>
|
||||
<h2 className="text-3xl font-bold text-white">OpenStock</h2>
|
||||
</Link>
|
||||
<nav className="hidden sm:block">
|
||||
@@ -16,7 +16,7 @@ const Header = () => {
|
||||
</nav>
|
||||
|
||||
{/* UserDropDown */}
|
||||
<UserDropdown/>
|
||||
<UserDropdown user={user}/>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
@@ -18,7 +18,7 @@ const NavItems = () => {
|
||||
<ul className="flex flex-col sm:flex-row p-2 gap-3 sm:gap-10 font-medium">
|
||||
{NAV_ITEMS.map(({href, label}) => (
|
||||
<li key={href}>
|
||||
<Link href={href} className={`hover:text-yellow-500 transition-colors ${isActive(href) ? 'text-gray-100' : ''}`}>
|
||||
<Link href={href} className={`hover:text-teal-500 transition-colors ${isActive(href) ? 'text-gray-100' : ''}`}>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -14,44 +14,40 @@ import {
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import {useRouter} from "next/navigation";
|
||||
import {LogOut} from "lucide-react";
|
||||
import {NAV_ITEMS} from "@/lib/constants";
|
||||
import NavItems from "@/components/NavItems";
|
||||
import {signOut} from "@/lib/actions/auth.actions";
|
||||
|
||||
const UserDropdown = () => {
|
||||
const UserDropdown = ({user} : {user: User}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSignOut = async() => {
|
||||
await signOut();
|
||||
router.push("/sign-in");
|
||||
}
|
||||
|
||||
const user = {
|
||||
name: "ODS",
|
||||
email: "opendevsociety@cc.cc",
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="flex items-center gap-3 text-gray-400 hover:text-yellow-500">
|
||||
<Button className="bg-gray-800 hover:bg-gray-800 flex items-center gap-3 text-gray-400 hover:text-teal-500">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src="https://media.licdn.com/dms/image/v2/D560BAQGHApE1Vtq6DA/company-logo_200_200/B56ZY1OFJOGcAI-/0/1744649609317/philosopai_in_logo?e=1761782400&v=beta&t=uLNK6v7h96sXybdT42cVK0cJSZaA8KVLw8JYO5fY4oQ" />
|
||||
<AvatarFallback className="bg-yellow-500 text-yellow-900 text-sm font-bold">
|
||||
<AvatarFallback className="bg-teal-500 text-yellow-900 text-sm font-bold">
|
||||
{user.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="hidden md:flex flex-col items-start">
|
||||
<span className="text-base font-medium text-gray-400">
|
||||
<span className="text-base font-medium text-gray-400 hover:text-teal-500">
|
||||
{user.name}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="text-gray-400">
|
||||
<DropdownMenuContent className="text-gray-400 bg-gray-800 relative right-10">
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex relative items-center gap-3 py-2">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src="https://media.licdn.com/dms/image/v2/D560BAQGHApE1Vtq6DA/company-logo_200_200/B56ZY1OFJOGcAI-/0/1744649609317/philosopai_in_logo?e=1761782400&v=beta&t=uLNK6v7h96sXybdT42cVK0cJSZaA8KVLw8JYO5fY4oQ" />
|
||||
<AvatarFallback className="bg-yellow-500 text-yellow-900 text-sm font-bold">
|
||||
<AvatarFallback className="bg-teal-500 text-yellow-900 text-sm font-bold">
|
||||
{user.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -66,11 +62,11 @@ const UserDropdown = () => {
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-gray-600"/>
|
||||
<DropdownMenuItem onClick={handleSignOut} className="text-gray-100 text-md font-medium focus:bg-transparent focus:text-yellow-500 transition-colors cursor-pointer">
|
||||
<LogOut className="h-4 w-4 mr-2 hidden sm:block"/>
|
||||
<DropdownMenuItem onClick={handleSignOut} className="text-gray-100 text-md font-medium focus:bg-transparent focus:text-teal-500 transition-colors cursor-pointer">
|
||||
<LogOut className="h-4 w-4 mr-2 hidden sm:block hover:text-teal-500"/>
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className=" hidden sm:block bg-gray-600"/>
|
||||
<DropdownMenuSeparator className=" block sm:hidden bg-gray-600"/>
|
||||
<nav className="sm:hidden">
|
||||
<NavItems/>
|
||||
</nav>
|
||||
|
||||
@@ -97,7 +97,7 @@ const CountrySelect = ({
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4 text-yellow-500',
|
||||
'mr-2 h-4 w-4 text-teal-500',
|
||||
value === country.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
|
||||
25
components/ui/sonner.tsx
Normal file
25
components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -35,4 +35,5 @@ export const connectToDatabase = async () => {
|
||||
}
|
||||
|
||||
console.log(`MongoDB Connected ${MONGODB_URI} in ${process.env.NODE_ENV}`);
|
||||
return cached.conn;
|
||||
}
|
||||
43
lib/actions/auth.actions.ts
Normal file
43
lib/actions/auth.actions.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
'use server';
|
||||
|
||||
import {auth} from "@/lib/better-auth/auth";
|
||||
import {inngest} from "@/lib/inngest/client";
|
||||
import {headers} from "next/headers";
|
||||
|
||||
export const signUpWithEmail = async ({ email, password, fullName, country, investmentGoals, riskTolerance, preferredIndustry }: SignUpFormData) => {
|
||||
try {
|
||||
const response = await auth.api.signUpEmail({ body: { email, password, name: fullName } })
|
||||
|
||||
if(response) {
|
||||
await inngest.send({
|
||||
name: 'app/user.created',
|
||||
data: { email, name: fullName, country, investmentGoals, riskTolerance, preferredIndustry }
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true, data: response }
|
||||
} catch (e) {
|
||||
console.log('Sign up failed', e)
|
||||
return { success: false, error: 'Sign up failed' }
|
||||
}
|
||||
}
|
||||
|
||||
export const signInWithEmail = async ({ email, password }: SignInFormData) => {
|
||||
try {
|
||||
const response = await auth.api.signInEmail({ body: { email, password } })
|
||||
|
||||
return { success: true, data: response }
|
||||
} catch (e) {
|
||||
console.log('Sign in failed', e)
|
||||
return { success: false, error: 'Sign in failed' }
|
||||
}
|
||||
}
|
||||
|
||||
export const signOut = async () => {
|
||||
try {
|
||||
await auth.api.signOut({ headers: await headers() });
|
||||
} catch (e) {
|
||||
console.log('Sign out failed', e)
|
||||
return { success: false, error: 'Sign out failed' }
|
||||
}
|
||||
}
|
||||
41
lib/better-auth/auth.ts
Normal file
41
lib/better-auth/auth.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import {mongodbAdapter} from "better-auth/adapters/mongodb";
|
||||
import {connectToDatabase} from "@/database/mongoose";
|
||||
import {nextCookies} from "better-auth/next-js";
|
||||
|
||||
|
||||
let authInstance: ReturnType<typeof betterAuth> | null = null;
|
||||
|
||||
|
||||
export const getAuth = async () => {
|
||||
if(authInstance) {
|
||||
return authInstance;
|
||||
}
|
||||
|
||||
const mongoose = await connectToDatabase();
|
||||
const db = mongoose.connection;
|
||||
|
||||
if (!db) {
|
||||
throw new Error("MongoDB connection not found!");
|
||||
}
|
||||
|
||||
authInstance = betterAuth({
|
||||
database: mongodbAdapter(db as any),
|
||||
secret: process.env.BETER_AUTH_SECRET,
|
||||
baseURL: process.env.BETTER_AUTH_URL,
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
disableSignUp: false,
|
||||
requireEmailVerification: false,
|
||||
minPasswordLength: 8,
|
||||
maxPasswordLength: 128,
|
||||
autoSignIn: true,
|
||||
},
|
||||
plugins: [nextCookies()],
|
||||
|
||||
});
|
||||
|
||||
return authInstance;
|
||||
}
|
||||
|
||||
export const auth = await getAuth();
|
||||
6
lib/inngest/client.ts
Normal file
6
lib/inngest/client.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {Inngest} from "inngest"
|
||||
|
||||
export const inngest = new Inngest({
|
||||
id: "openStock",
|
||||
ai: {gemini: {apiKey: process.env.GEMINI_API_KEY}}
|
||||
})
|
||||
48
lib/inngest/functions.ts
Normal file
48
lib/inngest/functions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {inngest} from '@/lib/inngest/client';
|
||||
import {PERSONALIZED_WELCOME_EMAIL_PROMPT} from "@/lib/inngest/prompts";
|
||||
import {sendWelcomeEmail} from "@/lib/nodemailer";
|
||||
|
||||
export const sendSignUpEmail = inngest.createFunction(
|
||||
{id: 'sign-up-email'},
|
||||
{event: 'app/user.created'},
|
||||
async ({event, step}) => {
|
||||
const userProfile = `
|
||||
- Country: ${event.data.country}
|
||||
- Investment goals: ${event.data.investmentGoals}
|
||||
- Risk tolerance: ${event.data.riskTolerance}
|
||||
- Preferred industry: ${event.data.preferredIndustry}
|
||||
`
|
||||
|
||||
const prompt = PERSONALIZED_WELCOME_EMAIL_PROMPT.replace('{{userProfile}}', userProfile);
|
||||
|
||||
const response = await step.ai.infer('generate-welcome-intro', {
|
||||
model: step.ai.models.gemini({model: 'gemini-2.5-flash-lite'}),
|
||||
body: {
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{text: prompt}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
await step.run('send-welcome-email', async() => {
|
||||
const part = response.candidates?.[0]?.content?.parts?.[0];
|
||||
const introText = (part && 'text' in part ? part.text : null) || 'Thanks for joining OpenStock and be part of this initiative by Open Dev Society';
|
||||
|
||||
const {data: {email, name}} = event;
|
||||
return await sendWelcomeEmail({email, name, intro: introText });
|
||||
|
||||
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Welcome email sent successfully!',
|
||||
}
|
||||
}
|
||||
)
|
||||
230
lib/inngest/prompts.ts
Normal file
230
lib/inngest/prompts.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
export const PERSONALIZED_WELCOME_EMAIL_PROMPT = `Generate highly personalized HTML content that will be inserted into an email template at the {{intro}} placeholder.
|
||||
|
||||
User profile data:
|
||||
{{userProfile}}
|
||||
|
||||
PERSONALIZATION REQUIREMENTS:
|
||||
You MUST create content that is obviously tailored to THIS specific user by:
|
||||
|
||||
IMPORTANT: Do NOT start the personalized content with "Welcome" since the email header already says "Welcome aboard {{name}}". Use alternative openings like "Thanks for joining", "Great to have you", "You're all set", "Perfect timing", etc.
|
||||
|
||||
1. **Direct Reference to User Details**: Extract and use specific information from their profile:
|
||||
- Their exact investment goals or objectives
|
||||
- Their stated risk tolerance level
|
||||
- Their preferred sectors/industries mentioned
|
||||
- Their experience level or background
|
||||
- Any specific stocks/companies they're interested in
|
||||
- Their investment timeline (short-term, long-term, retirement)
|
||||
|
||||
2. **Contextual Messaging**: Create content that shows you understand their situation:
|
||||
- New investors → Reference learning/starting their journey
|
||||
- Experienced traders → Reference advanced tools/strategy enhancement
|
||||
- Retirement planning → Reference building wealth over time
|
||||
- Specific sectors → Reference those exact industries by name
|
||||
- Conservative approach → Reference safety and informed decisions
|
||||
- Aggressive approach → Reference opportunities and growth potential
|
||||
|
||||
3. **Personal Touch**: Make it feel like it was written specifically for them:
|
||||
- Use their goals in your messaging
|
||||
- Reference their interests directly
|
||||
- Connect features to their specific needs
|
||||
- Make them feel understood and seen
|
||||
|
||||
CRITICAL FORMATTING REQUIREMENTS:
|
||||
- Return ONLY clean HTML content with NO markdown, NO code blocks, NO backticks
|
||||
- Use SINGLE paragraph only: <p class="mobile-text" style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">content</p>
|
||||
- Write exactly TWO sentences (add one more sentence than current single sentence)
|
||||
- Keep total content between 35-50 words for readability
|
||||
- Use <strong> for key personalized elements (their goals, sectors, etc.)
|
||||
- DO NOT include "Here's what you can do right now:" as this is already in the template
|
||||
- Make every word count toward personalization
|
||||
- Second sentence should add helpful context or reinforce the personalization
|
||||
|
||||
Example personalized outputs (showing obvious customization with TWO sentences):
|
||||
<p class="mobile-text" style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">Thanks for joining Signalist! As someone focused on <strong>technology growth stocks</strong>, you'll love our real-time alerts for companies like the ones you're tracking. We'll help you spot opportunities before they become mainstream news.</p>
|
||||
|
||||
<p class="mobile-text" style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">Great to have you aboard! Perfect for your <strong>conservative retirement strategy</strong> — we'll help you monitor dividend stocks without overwhelming you with noise. You can finally track your portfolio progress with confidence and clarity.</p>
|
||||
|
||||
<p class="mobile-text" style="margin: 0 0 30px 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">You're all set! Since you're new to investing, we've designed simple tools to help you build confidence while learning the <strong>healthcare sector</strong> you're interested in. Our beginner-friendly alerts will guide you without the confusing jargon.</p>`
|
||||
|
||||
export const NEWS_SUMMARY_EMAIL_PROMPT = `Generate HTML content for a market news summary email that will be inserted into the NEWS_SUMMARY_EMAIL_TEMPLATE at the {{newsContent}} placeholder.
|
||||
|
||||
News data to summarize:
|
||||
{{newsData}}
|
||||
|
||||
CRITICAL FORMATTING REQUIREMENTS:
|
||||
- Return ONLY clean HTML content with NO markdown, NO code blocks, NO backticks
|
||||
- Structure content with clear sections using proper HTML headings and paragraphs
|
||||
- Use these specific CSS classes and styles to match the email template:
|
||||
|
||||
SECTION HEADINGS (for categories like "Market Highlights", "Top Movers", etc.):
|
||||
<h3 class="mobile-news-title dark-text" style="margin: 30px 0 15px 0; font-size: 18px; font-weight: 600; color: #f8f9fa; line-height: 1.3;">Section Title</h3>
|
||||
|
||||
PARAGRAPHS (for news content):
|
||||
<p class="mobile-text dark-text-secondary" style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">Content goes here</p>
|
||||
|
||||
STOCK/COMPANY MENTIONS:
|
||||
<strong style="color: #FDD458;">Stock Symbol</strong> for ticker symbols
|
||||
<strong style="color: #CCDADC;">Company Name</strong> for company names
|
||||
|
||||
PERFORMANCE INDICATORS:
|
||||
Use 📈 for gains, 📉 for losses, 📊 for neutral/mixed
|
||||
|
||||
NEWS ARTICLE STRUCTURE:
|
||||
For each individual news item within a section, use this structure:
|
||||
1. Article container with visual styling and icon
|
||||
2. Article title as a subheading
|
||||
3. Key takeaways in bullet points (2-3 actionable insights)
|
||||
4. "What this means" section for context
|
||||
5. "Read more" link to the original article
|
||||
6. Visual divider between articles
|
||||
|
||||
ARTICLE CONTAINER:
|
||||
Wrap each article in a clean, simple container:
|
||||
<div class="dark-info-box" style="background-color: #212328; padding: 24px; margin: 20px 0; border-radius: 8px;">
|
||||
|
||||
ARTICLE TITLES:
|
||||
<h4 class="dark-text" style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #FFFFFF; line-height: 1.4;">
|
||||
Article Title Here
|
||||
</h4>
|
||||
|
||||
BULLET POINTS (minimum 3 concise insights):
|
||||
Use this format with clear, concise explanations (no label needed):
|
||||
<ul style="margin: 16px 0 20px 0; padding-left: 0; margin-left: 0; list-style: none;">
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>Clear, concise explanation in simple terms that's easy to understand quickly.
|
||||
</li>
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>Brief explanation with key numbers and what they mean in everyday language.
|
||||
</li>
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>Simple takeaway about what this means for regular people's money.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
INSIGHT SECTION:
|
||||
Add simple context explanation:
|
||||
<div style="background-color: #141414; border: 1px solid #374151; padding: 15px; border-radius: 6px; margin: 16px 0;">
|
||||
<p class="dark-text-secondary" style="margin: 0; font-size: 14px; color: #CCDADC; line-height: 1.4;">💡 <strong style="color: #FDD458;">Bottom Line:</strong> Simple explanation of why this news matters to your money in everyday language.</p>
|
||||
</div>
|
||||
|
||||
READ MORE BUTTON:
|
||||
<div style="margin: 20px 0 0 0;">
|
||||
<a href="ARTICLE_URL" style="color: #FDD458; text-decoration: none; font-weight: 500; font-size: 14px;" target="_blank" rel="noopener noreferrer">Read Full Story →</a>
|
||||
</div>
|
||||
|
||||
ARTICLE DIVIDER:
|
||||
Close each article container:
|
||||
</div>
|
||||
|
||||
SECTION DIVIDERS:
|
||||
Between major sections, use:
|
||||
<div style="border-top: 1px solid #374151; margin: 32px 0 24px 0;"></div>
|
||||
|
||||
Content guidelines:
|
||||
- Organize news into logical sections with icons (📊 Market Overview, 📈 Top Gainers, 📉 Top Losers, 🔥 Breaking News, 💼 Earnings Reports, 🏛️ Economic Data, etc.)
|
||||
- NEVER repeat section headings - use each section type only once per email
|
||||
- For each news article, include its actual headline/title from the news data
|
||||
- Provide MINIMUM 3 CONCISE bullet points (NO "Key Takeaways" label - start directly with bullets)
|
||||
- Each bullet should be SHORT and EASY TO UNDERSTAND - one clear sentence preferred
|
||||
- Use PLAIN ENGLISH - avoid jargon, complex financial terms, or insider language
|
||||
- Explain concepts as if talking to someone new to investing
|
||||
- Include specific numbers but explain what they mean in simple terms
|
||||
- Add "Bottom Line" context in everyday language anyone can understand
|
||||
- Use clean, light design with yellow bullets for better readability
|
||||
- Make each article easy to scan with clear spacing and structure
|
||||
- Always include simple "Read Full Story" buttons with actual URLs
|
||||
- Focus on PRACTICAL insights regular people can understand and use
|
||||
- Explain what the news means for regular investors' money
|
||||
- Keep language conversational and accessible to everyone
|
||||
- Prioritize BREVITY and CLARITY over detailed explanations
|
||||
|
||||
Example structure:
|
||||
<h3 class="mobile-news-title dark-text" style="margin: 30px 0 15px 0; font-size: 20px; font-weight: 600; color: #f8f9fa; line-height: 1.3;">📊 Market Overview</h3>
|
||||
|
||||
<div class="dark-info-box" style="background-color: #212328; padding: 24px; margin: 20px 0; border-radius: 8px;">
|
||||
<h4 class="dark-text" style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #FDD458; line-height: 1.4;">
|
||||
Stock Market Had Mixed Results Today
|
||||
</h4>
|
||||
|
||||
<ul style="margin: 16px 0 20px 0; padding-left: 0; margin-left: 0; list-style: none;">
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>Tech stocks like Apple went up 1.2% today, which is good news for tech investors.
|
||||
</li>
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>Traditional companies went down 0.3%, showing investors prefer tech right now.
|
||||
</li>
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>High trading volume (12.4 billion shares) shows investors are confident and active.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style="background-color: #141414; border: 1px solid #374151; padding: 15px; border-radius: 6px; margin: 16px 0;">
|
||||
<p class="dark-text-secondary" style="margin: 0; font-size: 14px; color: #CCDADC; line-height: 1.4;">💡 <strong style="color: #FDD458;">Bottom Line:</strong> If you own tech stocks, today was good for you. If you're thinking about investing, tech companies might be a smart choice right now.</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 20px 0 0 0;">
|
||||
<a href="https://example.com/article1" style="color: #FDD458; text-decoration: none; font-weight: 500; font-size: 14px;" target="_blank" rel="noopener noreferrer">Read Full Story →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px solid #374151; margin: 32px 0 24px 0;"></div>
|
||||
|
||||
<h3 class="mobile-news-title dark-text" style="margin: 30px 0 15px 0; font-size: 20px; font-weight: 600; color: #f8f9fa; line-height: 1.3;">📈 Top Gainers</h3>
|
||||
|
||||
<div class="dark-info-box" style="background-color: #212328; padding: 24px; margin: 20px 0; border-radius: 8px;">
|
||||
<h4 class="dark-text" style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #FDD458; line-height: 1.4;">
|
||||
Apple Stock Jumped After Great Earnings Report
|
||||
</h4>
|
||||
|
||||
<ul style="margin: 16px 0 20px 0; padding-left: 0; margin-left: 0; list-style: none;">
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>Apple stock jumped 5.2% after beating earnings expectations.
|
||||
</li>
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>iPhone sales expected to grow 8% next quarter despite economic uncertainty.
|
||||
</li>
|
||||
<li class="dark-text-secondary" style="margin: 0 0 16px 0; padding: 0; margin-left: 0; font-size: 16px; line-height: 1.6; color: #CCDADC;">
|
||||
<span style="color: #FDD458; font-weight: bold; font-size: 20px; margin-right: 8px;">•</span>App store and services revenue hit $22.3 billion (up 14%), providing steady income.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style="background-color: #141414; border: 1px solid #374151; padding: 15px; border-radius: 6px; margin: 16px 0;">
|
||||
<p class="dark-text-secondary" style="margin: 0; font-size: 14px; color: #CCDADC; line-height: 1.4;">💡 <strong style="color: #FDD458;">Bottom Line:</strong> Apple is making money in different ways (phones AND services), so it's a pretty safe stock to own even when the economy gets shaky.</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 20px 0 0 0;">
|
||||
<a href="https://example.com/article2" style="color: #FDD458; text-decoration: none; font-weight: 500; font-size: 14px;" target="_blank" rel="noopener noreferrer">Read Full Story →</a>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
export const TRADINGVIEW_SYMBOL_MAPPING_PROMPT = `You are an expert in financial markets and trading platforms. Your task is to find the correct TradingView symbol that corresponds to a given Finnhub stock symbol.
|
||||
|
||||
Stock information from Finnhub:
|
||||
Symbol: {{symbol}}
|
||||
Company: {{company}}
|
||||
Exchange: {{exchange}}
|
||||
Currency: {{currency}}
|
||||
Country: {{country}}
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. TradingView uses specific symbol formats that may differ from Finnhub
|
||||
2. For US stocks: Usually just the symbol (e.g., AAPL for Apple)
|
||||
3. For international stocks: Often includes exchange prefix (e.g., NASDAQ:AAPL, NYSE:MSFT, LSE:BARC)
|
||||
4. Some symbols may have suffixes for different share classes
|
||||
5. ADRs and foreign stocks may have different symbol formats
|
||||
|
||||
RESPONSE FORMAT:
|
||||
Return ONLY a valid JSON object with this exact structure:
|
||||
{
|
||||
"tradingViewSymbol": "EXCHANGE:SYMBOL",
|
||||
"confidence": "high|medium|low",
|
||||
"reasoning": "Brief explanation of why this mapping is correct"
|
||||
}
|
||||
|
||||
EXAMPLES:
|
||||
- Apple Inc. (AAPL) from Finnhub → {"tradingViewSymbol": "NASDAQ:AAPL", "confidence": "high", "reasoning": "Apple trades on NASDAQ as AAPL"}
|
||||
- Microsoft Corp (MSFT) from Finnhub → {"tradingViewSymbol": "NASDAQ:MSFT", "confidence": "high", "reasoning": "Microsoft trades on NASDAQ as MSFT"}
|
||||
- Barclays PLC (BARC.L) from Finnhub → {"tradingViewSymbol": "LSE:BARC", "confidence": "high", "reasoning": "Barclays trades on London Stock Exchange as BARC"}
|
||||
|
||||
Your response must be valid JSON only. Do not include any other text.`
|
||||
28
lib/nodemailer/index.ts
Normal file
28
lib/nodemailer/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
import {WELCOME_EMAIL_TEMPLATE} from "@/lib/nodemailer/templates";
|
||||
|
||||
|
||||
export const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.NODEMAILER_EMAIL,
|
||||
pass: process.env.NODEMAILER_PASSWORD,
|
||||
}
|
||||
})
|
||||
|
||||
export const sendWelcomeEmail = async ({email, name, intro} : WelcomeEmailData) => {
|
||||
const htmlTemplate = WELCOME_EMAIL_TEMPLATE
|
||||
.replace('{{name}}', name)
|
||||
.replace('{{intro}}', intro);
|
||||
|
||||
|
||||
const mailOptions = {
|
||||
from: `"Openstock" <opendevsociety@gamil.com>`,
|
||||
to: email,
|
||||
subject: 'Welcome to OpenStock - your open-source stock market toolkit',
|
||||
text: 'Thanks for joining Openstock and believing in this initiative by Open Dev Society',
|
||||
html: htmlTemplate,
|
||||
}
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
}
|
||||
1110
lib/nodemailer/templates.ts
Normal file
1110
lib/nodemailer/templates.ts
Normal file
File diff suppressed because it is too large
Load Diff
19
middleware/index.ts
Normal file
19
middleware/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getSessionCookie } from "better-auth/cookies";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const sessionCookie = getSessionCookie(request);
|
||||
|
||||
// Check cookie presence - prevents obviously unauthorized users
|
||||
if (!sessionCookie) {
|
||||
return NextResponse.redirect(new URL('/sign-in', request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!api|_next/static|_next/image|favicon.ico|sign-in|sign-up|assets).*)',
|
||||
],
|
||||
};
|
||||
4108
package-lock.json
generated
4108
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,26 +17,32 @@
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"better-auth": "^1.3.25",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"country-data-list": "^1.5.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"inngest": "^3.44.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mongodb": "^6.20.0",
|
||||
"mongoose": "^8.19.0",
|
||||
"next": "15.5.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nodemailer": "^7.0.6",
|
||||
"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",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-select-country-list": "^2.2.3",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 41 KiB |
Reference in New Issue
Block a user