diff --git a/.env b/.env
deleted file mode 100644
index 1d44286e25..0000000000
--- a/.env
+++ /dev/null
@@ -1,45 +0,0 @@
-# Domain
-# This would be set to the production domain with an env var on deployment
-# used by Traefik to transmit traffic and aqcuire TLS certificates
-DOMAIN=localhost
-# To test the local Traefik config
-# DOMAIN=localhost.tiangolo.com
-
-# Used by the backend to generate links in emails to the frontend
-FRONTEND_HOST=http://localhost:5173
-# In staging and production, set this env var to the frontend host, e.g.
-# FRONTEND_HOST=https://dashboard.example.com
-
-# Environment: local, staging, production
-ENVIRONMENT=local
-
-PROJECT_NAME="Full Stack FastAPI Project"
-STACK_NAME=full-stack-fastapi-project
-
-# Backend
-BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
-SECRET_KEY=changethis
-FIRST_SUPERUSER=admin@example.com
-FIRST_SUPERUSER_PASSWORD=changethis
-
-# Emails
-SMTP_HOST=
-SMTP_USER=
-SMTP_PASSWORD=
-EMAILS_FROM_EMAIL=info@example.com
-SMTP_TLS=True
-SMTP_SSL=False
-SMTP_PORT=587
-
-# Postgres
-POSTGRES_SERVER=localhost
-POSTGRES_PORT=5432
-POSTGRES_DB=app
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=changethis
-
-SENTRY_DSN=
-
-# Configure these with your own Docker registry images
-DOCKER_IMAGE_BACKEND=backend
-DOCKER_IMAGE_FRONTEND=frontend
diff --git a/.gitignore b/.gitignore
index a6dd346572..9522de3e5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@ node_modules/
/playwright-report/
/blob-report/
/playwright/.cache/
+
+.env
+
diff --git a/backend/.dockerignore b/backend/.dockerignore
index c0de4abf73..a9c50f229c 100644
--- a/backend/.dockerignore
+++ b/backend/.dockerignore
@@ -6,3 +6,7 @@ app.egg-info
.coverage
htmlcov
.venv
+
+.env
+.env.*
+*.env
\ No newline at end of file
diff --git a/frontend/src/components/Common/Navbar.tsx b/frontend/src/components/Common/Navbar.tsx
index 7e952e005e..4fe382063a 100644
--- a/frontend/src/components/Common/Navbar.tsx
+++ b/frontend/src/components/Common/Navbar.tsx
@@ -19,7 +19,7 @@ function Navbar() {
top={0}
p={4}
>
-
+
diff --git a/frontend/src/components/Common/NotFound.tsx b/frontend/src/components/Common/NotFound.tsx
index 9e4f18528e..4266552ab8 100644
--- a/frontend/src/components/Common/NotFound.tsx
+++ b/frontend/src/components/Common/NotFound.tsx
@@ -31,7 +31,7 @@ const NotFound = () => {
The page you are looking for was not found.
-
+
Go Back
diff --git a/frontend/src/components/Common/SidebarItems.tsx b/frontend/src/components/Common/SidebarItems.tsx
index 13f71495f5..d62c39dd8e 100644
--- a/frontend/src/components/Common/SidebarItems.tsx
+++ b/frontend/src/components/Common/SidebarItems.tsx
@@ -7,7 +7,7 @@ import type { IconType } from "react-icons/lib"
import type { UserPublic } from "@/client"
const items = [
- { icon: FiHome, title: "Dashboard", path: "/" },
+ { icon: FiHome, title: "Dashboard", path: "/dashboard" },
{ icon: FiBriefcase, title: "Items", path: "/items" },
{ icon: FiSettings, title: "User Settings", path: "/settings" },
]
diff --git a/frontend/src/components/Landing/OddsCard.tsx b/frontend/src/components/Landing/OddsCard.tsx
new file mode 100644
index 0000000000..770cb56e3b
--- /dev/null
+++ b/frontend/src/components/Landing/OddsCard.tsx
@@ -0,0 +1,280 @@
+import { Box, Button, Flex, Image, Text } from "@chakra-ui/react"
+
+import { useColorModeValue } from "@/components/ui/color-mode"
+
+type MarketKey = "home" | "draw" | "away"
+type OverUnderKey = "over" | "under"
+
+interface BookInfo {
+ name: string
+ logoUrl?: string
+}
+
+interface Market {
+ value: string
+ book?: BookInfo
+}
+
+export interface OddsCardProps {
+ homeTeam: string
+ awayTeam: string
+ marketLabel?: string
+ home: Market
+ draw: Market
+ away: Market
+ onSelect?: (market: MarketKey) => void
+}
+
+export interface OverUnderOddsCardProps {
+ homeTeam: string
+ awayTeam: string
+ marketLabel?: string
+ line?: string
+ over: Market
+ under: Market
+ onSelect?: (market: OverUnderKey) => void
+}
+
+interface OddsPillProps extends Market {
+ label: string
+ onSelect?: () => void
+}
+
+function BookBadge({ logoUrl, name }: BookInfo) {
+ const badgeBg = useColorModeValue("gray.100", "gray.700")
+ const badgeColor = useColorModeValue("gray.600", "gray.300")
+
+ return (
+
+ {logoUrl ? (
+
+ ) : (
+ {name}
+ )}
+
+ )
+}
+
+function OddsPill({ value, book, label, onSelect }: OddsPillProps) {
+ const borderColor = useColorModeValue("gray.200", "gray.600")
+ const hoverBg = useColorModeValue("gray.100", "gray.700")
+ const textColor = useColorModeValue("teal.600", "teal.200")
+
+ return (
+
+
+ {value}
+
+ {book ? : {label} }
+
+ )
+}
+
+function MarketColumn({
+ title,
+ market,
+ onSelect,
+}: {
+ title: string
+ market: Market
+ onSelect?: () => void
+}) {
+ return (
+
+
+ {title}
+
+
+
+ )
+}
+
+export function OddsCard({
+ homeTeam,
+ awayTeam,
+ marketLabel = "Moneyline",
+ home,
+ draw,
+ away,
+ onSelect,
+}: OddsCardProps) {
+ const wrapperBg = useColorModeValue("gray.50", "gray.800")
+ const borderColor = useColorModeValue("gray.200", "gray.600")
+
+ return (
+
+
+
+
+ {homeTeam}
+
+
+ VS
+
+
+ {awayTeam}
+
+
+
+
+ {marketLabel}
+
+
+ onSelect?.("home")} />
+ onSelect?.("draw")} />
+ onSelect?.("away")} />
+
+
+
+
+ )
+}
+
+export function OverUnderOddsCard({
+ homeTeam,
+ awayTeam,
+ marketLabel = "Total",
+ line,
+ over,
+ under,
+ onSelect,
+}: OverUnderOddsCardProps) {
+ const wrapperBg = useColorModeValue("gray.50", "gray.800")
+ const borderColor = useColorModeValue("gray.200", "gray.600")
+
+ return (
+
+
+
+
+ {homeTeam}
+
+
+ VS
+
+
+ {awayTeam}
+
+
+
+
+ {marketLabel}
+
+ {line && (
+
+ {line}
+
+ )}
+
+ onSelect?.("over")} />
+ onSelect?.("under")} />
+
+
+
+
+ )
+}
+
+export default OddsCard
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts
index 5344493d49..e058a4f1e5 100644
--- a/frontend/src/hooks/useAuth.ts
+++ b/frontend/src/hooks/useAuth.ts
@@ -51,7 +51,7 @@ const useAuth = () => {
const loginMutation = useMutation({
mutationFn: login,
onSuccess: () => {
- navigate({ to: "/" })
+ navigate({ to: "/dashboard" })
},
onError: (err: ApiError) => {
handleError(err)
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 8849130b4c..fd1514f9c5 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -13,10 +13,12 @@ import { Route as SignupRouteImport } from './routes/signup'
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as RecoverPasswordRouteImport } from './routes/recover-password'
import { Route as LoginRouteImport } from './routes/login'
+import { Route as LandingRouteImport } from './routes/landing'
import { Route as LayoutRouteImport } from './routes/_layout'
-import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings'
import { Route as LayoutItemsRouteImport } from './routes/_layout/items'
+import { Route as LayoutDashboardRouteImport } from './routes/_layout/dashboard'
import { Route as LayoutAdminRouteImport } from './routes/_layout/admin'
const SignupRoute = SignupRouteImport.update({
@@ -39,14 +41,19 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
+const LandingRoute = LandingRouteImport.update({
+ id: '/landing',
+ path: '/landing',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
-const LayoutIndexRoute = LayoutIndexRouteImport.update({
+const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
- getParentRoute: () => LayoutRoute,
+ getParentRoute: () => rootRouteImport,
} as any)
const LayoutSettingsRoute = LayoutSettingsRouteImport.update({
id: '/settings',
@@ -58,6 +65,11 @@ const LayoutItemsRoute = LayoutItemsRouteImport.update({
path: '/items',
getParentRoute: () => LayoutRoute,
} as any)
+const LayoutDashboardRoute = LayoutDashboardRouteImport.update({
+ id: '/dashboard',
+ path: '/dashboard',
+ getParentRoute: () => LayoutRoute,
+} as any)
const LayoutAdminRoute = LayoutAdminRouteImport.update({
id: '/admin',
path: '/admin',
@@ -65,73 +77,87 @@ const LayoutAdminRoute = LayoutAdminRouteImport.update({
} as any)
export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/landing': typeof LandingRoute
'/login': typeof LoginRoute
'/recover-password': typeof RecoverPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/signup': typeof SignupRoute
'/admin': typeof LayoutAdminRoute
+ '/dashboard': typeof LayoutDashboardRoute
'/items': typeof LayoutItemsRoute
'/settings': typeof LayoutSettingsRoute
- '/': typeof LayoutIndexRoute
}
export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/landing': typeof LandingRoute
'/login': typeof LoginRoute
'/recover-password': typeof RecoverPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/signup': typeof SignupRoute
'/admin': typeof LayoutAdminRoute
+ '/dashboard': typeof LayoutDashboardRoute
'/items': typeof LayoutItemsRoute
'/settings': typeof LayoutSettingsRoute
- '/': typeof LayoutIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
+ '/': typeof IndexRoute
'/_layout': typeof LayoutRouteWithChildren
+ '/landing': typeof LandingRoute
'/login': typeof LoginRoute
'/recover-password': typeof RecoverPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/signup': typeof SignupRoute
'/_layout/admin': typeof LayoutAdminRoute
+ '/_layout/dashboard': typeof LayoutDashboardRoute
'/_layout/items': typeof LayoutItemsRoute
'/_layout/settings': typeof LayoutSettingsRoute
- '/_layout/': typeof LayoutIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
+ | '/'
+ | '/landing'
| '/login'
| '/recover-password'
| '/reset-password'
| '/signup'
| '/admin'
+ | '/dashboard'
| '/items'
| '/settings'
- | '/'
fileRoutesByTo: FileRoutesByTo
to:
+ | '/'
+ | '/landing'
| '/login'
| '/recover-password'
| '/reset-password'
| '/signup'
| '/admin'
+ | '/dashboard'
| '/items'
| '/settings'
- | '/'
id:
| '__root__'
+ | '/'
| '/_layout'
+ | '/landing'
| '/login'
| '/recover-password'
| '/reset-password'
| '/signup'
| '/_layout/admin'
+ | '/_layout/dashboard'
| '/_layout/items'
| '/_layout/settings'
- | '/_layout/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
LayoutRoute: typeof LayoutRouteWithChildren
+ LandingRoute: typeof LandingRoute
LoginRoute: typeof LoginRoute
RecoverPasswordRoute: typeof RecoverPasswordRoute
ResetPasswordRoute: typeof ResetPasswordRoute
@@ -168,6 +194,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
+ '/landing': {
+ id: '/landing'
+ path: '/landing'
+ fullPath: '/landing'
+ preLoaderRoute: typeof LandingRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/_layout': {
id: '/_layout'
path: ''
@@ -175,12 +208,12 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/_layout/': {
- id: '/_layout/'
+ '/': {
+ id: '/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof LayoutIndexRouteImport
- parentRoute: typeof LayoutRoute
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
}
'/_layout/settings': {
id: '/_layout/settings'
@@ -196,6 +229,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutItemsRouteImport
parentRoute: typeof LayoutRoute
}
+ '/_layout/dashboard': {
+ id: '/_layout/dashboard'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof LayoutDashboardRouteImport
+ parentRoute: typeof LayoutRoute
+ }
'/_layout/admin': {
id: '/_layout/admin'
path: '/admin'
@@ -208,23 +248,25 @@ declare module '@tanstack/react-router' {
interface LayoutRouteChildren {
LayoutAdminRoute: typeof LayoutAdminRoute
+ LayoutDashboardRoute: typeof LayoutDashboardRoute
LayoutItemsRoute: typeof LayoutItemsRoute
LayoutSettingsRoute: typeof LayoutSettingsRoute
- LayoutIndexRoute: typeof LayoutIndexRoute
}
const LayoutRouteChildren: LayoutRouteChildren = {
LayoutAdminRoute: LayoutAdminRoute,
+ LayoutDashboardRoute: LayoutDashboardRoute,
LayoutItemsRoute: LayoutItemsRoute,
LayoutSettingsRoute: LayoutSettingsRoute,
- LayoutIndexRoute: LayoutIndexRoute,
}
const LayoutRouteWithChildren =
LayoutRoute._addFileChildren(LayoutRouteChildren)
const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
LayoutRoute: LayoutRouteWithChildren,
+ LandingRoute: LandingRoute,
LoginRoute: LoginRoute,
RecoverPasswordRoute: RecoverPasswordRoute,
ResetPasswordRoute: ResetPasswordRoute,
diff --git a/frontend/src/routes/_layout/index.tsx b/frontend/src/routes/_layout/dashboard.tsx
similarity index 89%
rename from frontend/src/routes/_layout/index.tsx
rename to frontend/src/routes/_layout/dashboard.tsx
index 66e32e0b30..44e90d389c 100644
--- a/frontend/src/routes/_layout/index.tsx
+++ b/frontend/src/routes/_layout/dashboard.tsx
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"
import useAuth from "@/hooks/useAuth"
-export const Route = createFileRoute("/_layout/")({
+export const Route = createFileRoute("/_layout/dashboard")({
component: Dashboard,
})
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
new file mode 100644
index 0000000000..f08040d11a
--- /dev/null
+++ b/frontend/src/routes/index.tsx
@@ -0,0 +1,24 @@
+import { useEffect } from "react"
+
+import { createFileRoute, useNavigate } from "@tanstack/react-router"
+
+import { isLoggedIn } from "@/hooks/useAuth"
+
+export const Route = createFileRoute("/")({
+ component: RootRedirect,
+})
+
+function RootRedirect() {
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ navigate({
+ to: isLoggedIn() ? "/dashboard" : "/landing",
+ replace: true,
+ })
+ }, [navigate])
+
+ return null
+}
+
+export default RootRedirect
diff --git a/frontend/src/routes/landing.tsx b/frontend/src/routes/landing.tsx
new file mode 100644
index 0000000000..c500b5bbb7
--- /dev/null
+++ b/frontend/src/routes/landing.tsx
@@ -0,0 +1,171 @@
+import { Box, Button, Container, Flex, Heading, Text } from "@chakra-ui/react"
+import { Link as RouterLink, createFileRoute } from "@tanstack/react-router"
+
+import { isLoggedIn } from "@/hooks/useAuth"
+import { OddsCard, OverUnderOddsCard } from "@/components/Landing/OddsCard"
+import { ColorModeButton } from "@/components/ui/color-mode"
+import useCustomToast from "@/hooks/useCustomToast"
+
+export const Route = createFileRoute("/landing")({
+ component: LandingPage,
+})
+
+function LandingPage() {
+ const loggedIn = isLoggedIn()
+ const { showSuccessToast } = useCustomToast()
+
+ const featuredMarket = {
+ homeTeam: "Tampines Rovers",
+ awayTeam: "Pohang Steelers",
+ marketLabel: "Moneyline",
+ home: {
+ value: "+640",
+ book: {
+ name: "BC.GAME",
+ logoUrl: "https://dummyimage.com/60x18/eeeeee/333333&text=BC.GAME",
+ },
+ },
+ draw: {
+ value: "+350",
+ book: {
+ name: "MOSTBET",
+ logoUrl: "https://dummyimage.com/64x18/eeeeee/333333&text=MOSTBET",
+ },
+ },
+ away: {
+ value: "-129",
+ book: {
+ name: "betway",
+ logoUrl: "https://dummyimage.com/64x18/eeeeee/333333&text=betway",
+ },
+ },
+ }
+
+ const totalsMarket = {
+ homeTeam: "Real Betis",
+ awayTeam: "Sevilla FC",
+ marketLabel: "Total Goals",
+ line: "2.5",
+ over: {
+ value: "-105",
+ book: {
+ name: "FanDuel",
+ logoUrl: "https://dummyimage.com/64x18/eeeeee/333333&text=FanDuel",
+ },
+ },
+ under: {
+ value: "-110",
+ book: {
+ name: "DraftKings",
+ logoUrl: "https://dummyimage.com/64x18/eeeeee/333333&text=DraftKings",
+ },
+ },
+ }
+
+ return (
+
+ {/*
+
+
+
+
+ Seamless Arbitrage Insights
+
+
+ Track market opportunities, monitor performance, and make decisions
+ with confidence. All from a single, intuitive dashboard.
+
+
+
+
+ {loggedIn ? "Go to Dashboard" : "Get Started"}
+
+
+ {!loggedIn && (
+
+
+ Log In
+
+
+ )}
+
+ */}
+
+ {/*
+
+
+
+ */}
+
+
+
+
+
+ showSuccessToast(
+ `Selected ${market.toUpperCase()} market from the featured matchup.`,
+ )
+ }
+ />
+
+
+
+
+
+ showSuccessToast(
+ `Selected ${market.toUpperCase()} option on the featured totals market.`,
+ )
+ }
+ />
+
+
+ )
+}
+
+interface FeatureProps {
+ title: string
+ description: string
+}
+
+function Feature({ title, description }: FeatureProps) {
+ return (
+
+
+ {title}
+
+ {description}
+
+ )
+}
+
+export default LandingPage
diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx
index 2e0539d9d9..49f2d680e1 100644
--- a/frontend/src/routes/login.tsx
+++ b/frontend/src/routes/login.tsx
@@ -21,7 +21,7 @@ export const Route = createFileRoute("/login")({
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
- to: "/",
+ to: "/dashboard",
})
}
},
diff --git a/frontend/src/routes/recover-password.tsx b/frontend/src/routes/recover-password.tsx
index 084fbdd73a..b7144fc9b4 100644
--- a/frontend/src/routes/recover-password.tsx
+++ b/frontend/src/routes/recover-password.tsx
@@ -21,7 +21,7 @@ export const Route = createFileRoute("/recover-password")({
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
- to: "/",
+ to: "/dashboard",
})
}
},
diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx
index f55f49e287..04598696aa 100644
--- a/frontend/src/routes/reset-password.tsx
+++ b/frontend/src/routes/reset-password.tsx
@@ -20,7 +20,7 @@ export const Route = createFileRoute("/reset-password")({
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
- to: "/",
+ to: "/dashboard",
})
}
},
diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx
index 6e7890c485..5bc9708cb7 100644
--- a/frontend/src/routes/signup.tsx
+++ b/frontend/src/routes/signup.tsx
@@ -21,7 +21,7 @@ export const Route = createFileRoute("/signup")({
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
- to: "/",
+ to: "/dashboard",
})
}
},