Merge pull request #38 from Sosokker/front-end

Add specific project investment page and several tests with playwright + Refactor invest page code + Add search system
This commit is contained in:
Pattadon L. 2024-10-03 11:49:14 +07:00 committed by GitHub
commit 5ad8281d58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 3478 additions and 450 deletions

27
.github/workflows/playwright.yml vendored Normal file
View File

@ -0,0 +1,27 @@
# name: Playwright Tests
# on:
# push:
# branches: [ main, master ]
# pull_request:
# branches: [ main, master ]
# jobs:
# test:
# timeout-minutes: 60
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: lts/*
# - name: Install dependencies
# run: npm ci
# - name: Install Playwright Browsers
# run: npx playwright install --with-deps
# - name: Run Playwright tests
# run: npx playwright test
# - uses: actions/upload-artifact@v4
# if: ${{ !cancelled() }}
# with:
# name: playwright-report
# path: playwright-report/
# retention-days: 30

10
.gitignore vendored
View File

@ -38,4 +38,12 @@ next-env.d.ts
.env
.env.local
.vscode
.vscode
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
storageState.json

1171
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,19 +10,27 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@supabase-cache-helpers/postgrest-react-query": "^1.10.1",
"@supabase/ssr": "^0.4.1",
"@supabase/supabase-js": "^2.45.2",
"@tanstack/react-query": "^5.59.0",
"@tanstack/react-query-devtools": "^5.59.0",
"b2d-ventures": "file:",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.2.0",
"lucide-react": "^0.428.0",
"next": "14.2.5",
@ -31,10 +39,12 @@
"react-countup": "^6.5.3",
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"recharts": "^2.12.7",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",

84
playwright.config.ts Normal file
View File

@ -0,0 +1,84 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: require.resolve('./test_util/global-setup'),
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
storageState: './storageState.json',
ignoreHTTPSErrors: true,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'],
storageState:"./storageState.json"
},
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});

View File

@ -1,21 +1,34 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@radix-ui/react-avatar':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.1
version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-hover-card':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-label':
specifier: ^2.1.0
version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-navigation-menu':
specifier: ^1.2.0
version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-progress':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-radio-group':
specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-select':
specifier: ^2.1.1
version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
@ -25,21 +38,36 @@ dependencies:
'@radix-ui/react-slot':
specifier: ^1.1.0
version: 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-tabs':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-tooltip':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@supabase-cache-helpers/postgrest-react-query':
specifier: ^1.10.1
version: 1.10.1(@supabase/postgrest-js@1.15.8)(@supabase/supabase-js@2.45.2)(@tanstack/react-query@5.59.0)(react@18.3.1)
'@supabase/ssr':
specifier: ^0.4.1
version: 0.4.1(@supabase/supabase-js@2.45.2)
'@supabase/supabase-js':
specifier: ^2.45.2
version: 2.45.2
'@tanstack/react-query':
specifier: ^5.59.0
version: 5.59.0(react@18.3.1)
'@tanstack/react-query-devtools':
specifier: ^5.59.0
version: 5.59.0(@tanstack/react-query@5.59.0)(react@18.3.1)
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
clsx:
specifier: ^2.1.1
version: 2.1.1
dotenv:
specifier: ^16.4.5
version: 16.4.5
embla-carousel-react:
specifier: ^8.2.0
version: 8.2.0(react@18.3.1)
@ -48,7 +76,7 @@ dependencies:
version: 0.428.0(react@18.3.1)
next:
specifier: 14.2.5
version: 14.2.5(react-dom@18.3.1)(react@18.3.1)
version: 14.2.5(@playwright/test@1.47.2)(react-dom@18.3.1)(react@18.3.1)
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.3.1)(react@18.3.1)
@ -64,6 +92,9 @@ dependencies:
react-hot-toast:
specifier: ^2.4.1
version: 2.4.1(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1)
recharts:
specifier: ^2.12.7
version: 2.12.7(react-dom@18.3.1)(react@18.3.1)
tailwind-merge:
specifier: ^2.5.2
version: 2.5.2
@ -72,6 +103,9 @@ dependencies:
version: 1.0.7(tailwindcss@3.4.10)
devDependencies:
'@playwright/test':
specifier: ^1.47.2
version: 1.47.2
'@types/node':
specifier: ^20
version: 20.16.2
@ -103,6 +137,21 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
/@babel/runtime-corejs3@7.25.7:
resolution: {integrity: sha512-gMmIEhg35sXk9Te5qbGp3W9YKrvLt3HV658/d3odWrHSqT0JeG5OzsJWFHRLiOohRyjRsJc/x03DhJm3i8VJxg==}
engines: {node: '>=6.9.0'}
dependencies:
core-js-pure: 3.38.1
regenerator-runtime: 0.14.1
dev: false
/@babel/runtime@7.25.7:
resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: false
/@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -346,6 +395,13 @@ packages:
requiresBuild: true
optional: true
/@playwright/test@1.47.2:
resolution: {integrity: sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==}
engines: {node: '>=18'}
hasBin: true
dependencies:
playwright: 1.47.2
/@radix-ui/number@1.1.0:
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
dev: false
@ -446,6 +502,52 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-context@1.1.1(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.4
react: 18.3.1
dev: false
/@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
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
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.6.0(@types/react@18.3.4)(react@18.3.1)
dev: false
/@radix-ui/react-direction@1.1.0(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies:
@ -483,6 +585,30 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
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
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==}
peerDependencies:
@ -522,6 +648,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.4
react: 18.3.1
dev: false
/@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
peerDependencies:
@ -586,6 +725,26 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==}
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
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==}
peerDependencies:
@ -706,6 +865,27 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
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
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
peerDependencies:
@ -727,6 +907,27 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
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
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@ -768,6 +969,35 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-radio-group@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==}
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
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
peerDependencies:
@ -870,6 +1100,33 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-tabs@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==}
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
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@types/react': 18.3.4
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-tooltip@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w==}
peerDependencies:
@ -1032,6 +1289,36 @@ packages:
resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==}
dev: true
/@supabase-cache-helpers/postgrest-core@0.8.1(@supabase/postgrest-js@1.15.8)(@supabase/supabase-js@2.45.2):
resolution: {integrity: sha512-WlmpQBBui54jQPQ9CwE7rdDcdPcMQLRl37aRJ+AEpEm47HMn0lJ/0cTuC8BT0aBMHK8BgOSx0dZ1QjgND1sB6g==}
peerDependencies:
'@supabase/postgrest-js': ^1.9.0
'@supabase/supabase-js': ^2.0.0
dependencies:
'@supabase/postgrest-js': 1.15.8
'@supabase/supabase-js': 2.45.2
fast-equals: 5.0.1
flat: 6.0.1
merge-anything: 5.1.7
xregexp: 5.1.1
dev: false
/@supabase-cache-helpers/postgrest-react-query@1.10.1(@supabase/postgrest-js@1.15.8)(@supabase/supabase-js@2.45.2)(@tanstack/react-query@5.59.0)(react@18.3.1):
resolution: {integrity: sha512-BGGrEKue6mSH0el1iO1+iOSuyGqQxsHQ2B8dXWlDvYGmU7rmw5AGqXqDWngMBX68LcoHVJEogdbXP0pH4+1Tuw==}
peerDependencies:
'@supabase/postgrest-js': ^1.9.0
'@tanstack/react-query': ^4.0.0 || ^5.0.0
react: ^16.11.0 || ^17.0.0 || ^18.0.0
dependencies:
'@supabase-cache-helpers/postgrest-core': 0.8.1(@supabase/postgrest-js@1.15.8)(@supabase/supabase-js@2.45.2)
'@supabase/postgrest-js': 1.15.8
'@tanstack/react-query': 5.59.0(react@18.3.1)
flat: 6.0.1
react: 18.3.1
transitivePeerDependencies:
- '@supabase/supabase-js'
dev: false
/@supabase/auth-js@2.64.4:
resolution: {integrity: sha512-9ITagy4WP4FLl+mke1rchapOH0RQpf++DI+WSG2sO1OFOZ0rW3cwAM0nCrMOxu+Zw4vJ4zObc08uvQrXx590Tg==}
dependencies:
@ -1111,6 +1398,76 @@ packages:
tslib: 2.7.0
dev: false
/@tanstack/query-core@5.59.0:
resolution: {integrity: sha512-WGD8uIhX6/deH/tkZqPNcRyAhDUqs729bWKoByYHSogcshXfFbppOdTER5+qY7mFvu8KEFJwT0nxr8RfPTVh0Q==}
dev: false
/@tanstack/query-devtools@5.58.0:
resolution: {integrity: sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==}
dev: false
/@tanstack/react-query-devtools@5.59.0(@tanstack/react-query@5.59.0)(react@18.3.1):
resolution: {integrity: sha512-Kz7577FQGU8qmJxROIT/aOwmkTcxfBqgTP6r1AIvuJxVMVHPkp8eQxWQ7BnfBsy/KTJHiV9vMtRVo1+R1tB3vg==}
peerDependencies:
'@tanstack/react-query': ^5.59.0
react: ^18 || ^19
dependencies:
'@tanstack/query-devtools': 5.58.0
'@tanstack/react-query': 5.59.0(react@18.3.1)
react: 18.3.1
dev: false
/@tanstack/react-query@5.59.0(react@18.3.1):
resolution: {integrity: sha512-YDXp3OORbYR+8HNQx+lf4F73NoiCmCcSvZvgxE29OifmQFk0sBlO26NWLHpcNERo92tVk3w+JQ53/vkcRUY1hA==}
peerDependencies:
react: ^18 || ^19
dependencies:
'@tanstack/query-core': 5.59.0
react: 18.3.1
dev: false
/@types/d3-array@3.2.1:
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
dev: false
/@types/d3-color@3.1.3:
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
dev: false
/@types/d3-ease@3.0.2:
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
dev: false
/@types/d3-interpolate@3.0.4:
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
dependencies:
'@types/d3-color': 3.1.3
dev: false
/@types/d3-path@3.1.0:
resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
dev: false
/@types/d3-scale@4.0.8:
resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
dependencies:
'@types/d3-time': 3.0.3
dev: false
/@types/d3-shape@3.1.6:
resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
dependencies:
'@types/d3-path': 3.1.0
dev: false
/@types/d3-time@3.0.3:
resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
dev: false
/@types/d3-timer@3.0.2:
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
dev: false
/@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
@ -1519,6 +1876,11 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/core-js-pure@3.38.1:
resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==}
requiresBuild: true
dev: false
/countup.js@2.8.0:
resolution: {integrity: sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==}
dev: false
@ -1539,6 +1901,77 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
/d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
dependencies:
internmap: 2.0.3
dev: false
/d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
dev: false
/d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
dev: false
/d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
dev: false
/d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
dev: false
/d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
dependencies:
d3-time: 3.1.0
dev: false
/d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
dev: false
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
@ -1593,6 +2026,10 @@ packages:
ms: 2.1.2
dev: true
/decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
dev: false
/deep-equal@2.2.3:
resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
engines: {node: '>= 0.4'}
@ -1670,6 +2107,18 @@ packages:
esutils: 2.0.3
dev: true
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
'@babel/runtime': 7.25.7
csstype: 3.1.3
dev: false
/dotenv@16.4.5:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'}
dev: false
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -2128,10 +2577,19 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: false
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-equals@5.0.1:
resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
engines: {node: '>=6.0.0'}
dev: false
/fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@ -2185,6 +2643,12 @@ packages:
rimraf: 3.0.2
dev: true
/flat@6.0.1:
resolution: {integrity: sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==}
engines: {node: '>=18'}
hasBin: true
dev: false
/flatted@3.3.1:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
dev: true
@ -2206,6 +2670,13 @@ packages:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
optional: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -2433,6 +2904,11 @@ packages:
side-channel: 1.0.6
dev: true
/internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
dev: false
/invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
dependencies:
@ -2626,6 +3102,11 @@ packages:
get-intrinsic: 1.2.4
dev: true
/is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
dev: false
/isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
dev: true
@ -2749,6 +3230,10 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@ -2766,6 +3251,13 @@ packages:
react: 18.3.1
dev: false
/merge-anything@5.1.7:
resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
engines: {node: '>=12.13'}
dependencies:
is-what: 4.1.16
dev: false
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -2838,7 +3330,7 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/next@14.2.5(react-dom@18.3.1)(react@18.3.1):
/next@14.2.5(@playwright/test@1.47.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==}
engines: {node: '>=18.17.0'}
hasBin: true
@ -2857,6 +3349,7 @@ packages:
optional: true
dependencies:
'@next/env': 14.2.5
'@playwright/test': 1.47.2
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001653
@ -3043,6 +3536,20 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
/playwright-core@1.47.2:
resolution: {integrity: sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==}
engines: {node: '>=18'}
hasBin: true
/playwright@1.47.2:
resolution: {integrity: sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==}
engines: {node: '>=18'}
hasBin: true
dependencies:
playwright-core: 1.47.2
optionalDependencies:
fsevents: 2.3.2
/possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
engines: {node: '>= 0.4'}
@ -3131,7 +3638,6 @@ packages:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
dev: true
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
@ -3176,7 +3682,6 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-remove-scroll-bar@2.3.6(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
@ -3213,6 +3718,38 @@ packages:
use-sidecar: 1.1.2(@types/react@18.3.4)(react@18.3.1)
dev: false
/react-remove-scroll@2.6.0(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.4
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.4)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.4)(react@18.3.1)
tslib: 2.7.0
use-callback-ref: 1.3.2(@types/react@18.3.4)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.4)(react@18.3.1)
dev: false
/react-smooth@4.0.1(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
fast-equals: 5.0.1
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1)
dev: false
/react-style-singleton@2.2.1(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@ -3230,6 +3767,20 @@ packages:
tslib: 2.7.0
dev: false
/react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
react: '>=16.6.0'
react-dom: '>=16.6.0'
dependencies:
'@babel/runtime': 7.25.7
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@ -3248,6 +3799,31 @@ packages:
dependencies:
picomatch: 2.3.1
/recharts-scale@0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
dependencies:
decimal.js-light: 2.5.1
dev: false
/recharts@2.12.7(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==}
engines: {node: '>=14'}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
clsx: 2.1.1
eventemitter3: 4.0.7
lodash: 4.17.21
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 16.13.1
react-smooth: 4.0.1(react-dom@18.3.1)(react@18.3.1)
recharts-scale: 0.4.5
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
dev: false
/reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
@ -3261,6 +3837,10 @@ packages:
which-builtin-type: 1.1.4
dev: true
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false
/regexp.prototype.flags@1.5.2:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
engines: {node: '>= 0.4'}
@ -3617,6 +4197,10 @@ packages:
dependencies:
any-promise: 1.3.0
/tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
dev: false
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -3766,6 +4350,25 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
/victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-ease': 3.0.2
'@types/d3-interpolate': 3.0.4
'@types/d3-scale': 4.0.8
'@types/d3-shape': 3.1.6
'@types/d3-time': 3.0.3
'@types/d3-timer': 3.0.2
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
@ -3871,6 +4474,12 @@ packages:
optional: true
dev: false
/xregexp@5.1.1:
resolution: {integrity: sha512-fKXeVorD+CzWvFs7VBuKTYIW63YD1e1osxwQ8caZ6o1jg6pDAbABDG54LCIq0j5cy7PjRvGIq6sef9DYPXpncg==}
dependencies:
'@babel/runtime-corejs3': 7.25.7
dev: false
/yaml@2.5.0:
resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==}
engines: {node: '>= 14'}
@ -3880,7 +4489,3 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { LoginButton } from "@/components/auth/loginButton";
import { LoginForm } from "@/components/auth/loginForm";
export default function Login() {
return (
@ -18,6 +19,8 @@ export default function Login() {
</CardHeader>
<CardContent className="flex flex-col gap-y-2 mx-28">
<p className="self-center font-semibold text-slate-800 dark:text-slate-200">Continue With</p>
<LoginForm />
<hr></hr>
<LoginButton />
</CardContent>
<CardFooter className="text-xs justify-center">

198
src/app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,198 @@
"use client";
import Image from "next/image";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Overview } from "@/components/ui/overview";
import { RecentFunds } from "@/components/recent-funds";
import { useState } from "react";
export default function Dashboard() {
const [graphType, setGraphType] = useState("line");
return (
<>
<div className="md:hidden">
<Image
src="/examples/dashboard-light.png"
width={1280}
height={866}
alt="Dashboard"
className="block dark:hidden"
/>
<Image
src="/examples/dashboard-dark.png"
width={1280}
height={866}
alt="Dashboard"
className="hidden dark:block"
/>
</div>
<div className="hidden flex-col md:flex">
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Funds Raised
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Profile Views
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">
+180.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Followers
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">
+19% from last month
</p>
</CardContent>
</Card>
{/* <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Now
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">
+201 since last hour
</p>
</CardContent>
</Card> */}
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<Overview graphType={graphType} />
{/* tab to switch between line and bar graph */}
<Tabs
defaultValue="line"
className="space-y-4 ml-[50%] mt-2"
>
<TabsList>
<TabsTrigger
value="line"
onClick={() => setGraphType("line")}
>
Line
</TabsTrigger>
<TabsTrigger
value="bar"
onClick={() => setGraphType("bar")}
>
Bar
</TabsTrigger>
</TabsList>
</Tabs>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Recent Funds</CardTitle>
<CardDescription>
You made 265 sales this month.
</CardDescription>
</CardHeader>
<CardContent>
<RecentFunds />
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
</div>
</>
);
}

View File

@ -1,18 +1,7 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useState } from "react";
import {
Clock3Icon,
MessageSquareIcon,
UserIcon,
UsersIcon,
} from "lucide-react";
import { Clock3Icon, MessageSquareIcon, UserIcon, UsersIcon } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { ExtendableCard } from "@/components/extendableCard";
@ -21,14 +10,14 @@ export default function Deals() {
const [contentTypeFilter, setContentTypeFilter] = useState("");
const [authorFilter, setAuthorFilter] = useState("");
const [groupsFilter, setGroupFilter] = useState("");
const [selectedTag, setSelectedTag] = useState("");
const data = [
{
name: "NVDA",
description:
"Founded in 1993, NVIDIA is a key innovator of computer graphics and AI technology",
description: "Founded in 1993, NVIDIA is a key innovator of computer graphics and AI technology",
joinDate: "December 2021",
location: "Bangkok, Thailand",
tags: null,
tags: ["AI", "Technology"],
minInvestment: 10000,
totalInvestor: 58400,
totalRaised: 9000000,
@ -39,7 +28,7 @@ export default function Deals() {
"Founded in 1976, Apple Inc. is a leading innovator in consumer electronics, software, and online services, known for products like the iPhone, MacBook, and the App Store.",
joinDate: "February 2020",
location: "Cupertino, California, USA",
tags: null,
tags: ["Consumer Electronics", "Software"],
minInvestment: 10000,
totalInvestor: 58400,
totalRaised: 9000000,
@ -50,30 +39,31 @@ export default function Deals() {
"Founded in 1998, Google LLC specializes in internet-related services and products, including search engines, online advertising, cloud computing, and the Android operating system.",
joinDate: "April 2019",
location: "Mountain View, California, USA",
tags: null,
tags: ["Internet", "Search Engine"],
minInvestment: 10000,
totalInvestor: 5000,
totalRaised: 1500000000,
},
{
name: "Microsoft Corporation",
description:
"Microsoft Corporation is a multinational technology company.",
description: "Microsoft Corporation is a multinational technology company.",
joinDate: "January 2018",
location: "California, USA",
tags: null,
tags: ["Technology", "Software"],
minInvestment: 250,
totalInvestor: 5000,
totalRaised: 1500000,
},
];
const filteredData = selectedTag ? data.filter((item) => item.tags.includes(selectedTag)) : data;
return (
<div>
<div className=" w-1/2 h-[250px] mt-10 ml-[15%]">
<h1 className="text-4xl font-bold">Investment Opportunities </h1>
<h1 className="text-4xl font-bold">Investment Opportunities</h1>
<br />
<p>Browse current investment opportunities on Republic. </p>
<p>Browse current investment opportunities on B2DVenture.</p>
<p>
All companies are <u>vetted & pass due diligence.</u>
</p>
@ -89,40 +79,22 @@ export default function Deals() {
<SelectItem value="Yesterday">Yesterday</SelectItem>
</SelectContent>
</Select>
<Select onValueChange={(value) => setContentTypeFilter(value)}>
<Select onValueChange={(value) => setSelectedTag(value)}>
{" "}
{/* Tag filtering */}
<SelectTrigger className="w-[180px]">
<MessageSquareIcon className="ml-2" />
<SelectValue placeholder="Content type" />
<SelectValue placeholder="Tags" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Blog">Blog</SelectItem>
<SelectItem value="Youtube">Youtube</SelectItem>
<SelectItem value="_">All Tags</SelectItem> {/* Reset filter */}
<SelectItem value="AI">AI</SelectItem>
<SelectItem value="Technology">Technology</SelectItem>
<SelectItem value="Consumer Electronics">Consumer Electronics</SelectItem>
<SelectItem value="Software">Software</SelectItem>
<SelectItem value="Internet">Internet</SelectItem>
</SelectContent>
</Select>
<Select onValueChange={(value) => setAuthorFilter(value)}>
<SelectTrigger className="w-[180px]">
<UserIcon className="ml-2" />
<SelectValue placeholder="Author" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Me">Me</SelectItem>
<SelectItem value="Charlie Puth">Charlie Puth</SelectItem>
</SelectContent>
</Select>
<Select onValueChange={(value) => setGroupFilter(value)}>
<SelectTrigger className="w-[180px]">
<UsersIcon />
<SelectValue placeholder="Sent to groups" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Group1">Group1</SelectItem>
<SelectItem value="Group2">Group2</SelectItem>
</SelectContent>
</Select>
{/* {postAtFilter}
{contentTypeFilter}
{authorFilter}
{groupsFilter} */}
</div>
<Separator className="mt-10" />
</div>
@ -133,7 +105,7 @@ export default function Deals() {
</div>
{/* block for all the deals */}
<div className="ml-[15%] mt-10 grid grid-cols-3">
{data.map((item, index) => (
{filteredData.map((item, index) => (
<ExtendableCard
key={index}
name={item.name}

180
src/app/find/page.tsx Normal file
View File

@ -0,0 +1,180 @@
"use client";
import { useSearchParams } from "next/navigation";
import { SupabaseClient } from "@supabase/supabase-js";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ExtendableCard } from "@/components/extendableCard";
import { Separator } from "@/components/ui/separator";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
interface ProjectInvestmentDetail {
minInvestment: number;
totalInvestment: number;
targetInvestment: number;
}
interface Project {
id: string;
projectName: string;
businessId: string;
investmentCount: number;
projectShortDescription: string;
publishedTime: string;
ProjectInvestmentDetail: ProjectInvestmentDetail[];
tags: string[];
}
interface Business {
id: string;
businessName: string;
joinedDate: string;
Projects: Project[];
}
function getBusinesses(client: SupabaseClient, query: string | null) {
return client.from("Business").select("id, businessName, joinedDate").ilike("businessName", `%${query}%`);
}
function getProjects(client: SupabaseClient, businessIds: string[]) {
return client
.from("Project")
.select(
`
id,
projectName,
businessId,
publishedTime,
projectShortDescription,
ProjectInvestmentDetail (
minInvestment,
totalInvestment,
targetInvestment
)
`
)
.in("businessId", businessIds);
}
function getTags(client: SupabaseClient, projectIds: string[]) {
return client.from("ItemTag").select("itemId, Tag (value)").in("itemId", projectIds);
}
function getInvestmentCounts(client: SupabaseClient, projectIds: string[]) {
return client.from("InvestmentDeal").select("*", { count: "exact", head: true }).in("projectId", projectIds);
}
export default function Find() {
const searchParams = useSearchParams();
const query = searchParams.get("query");
// const query = "neon";
let supabase = createSupabaseClient();
const {
data: businesses,
isLoading: isLoadingBusinesses,
error: businessError,
} = useQuery(getBusinesses(supabase, query));
const businessIds = businesses?.map((b) => b.id) || [];
const {
data: projects,
isLoading: isLoadingProjects,
error: projectError,
} = useQuery(getProjects(supabase, businessIds), { enabled: businessIds.length > 0 });
const projectIds = projects?.map((p) => p.id) || [];
const {
data: tags,
isLoading: isLoadingTags,
error: tagError,
} = useQuery(getTags(supabase, projectIds), { enabled: projectIds.length > 0 });
const {
data: investmentCounts,
isLoading: isLoadingInvestments,
error: investmentError,
} = useQuery(getInvestmentCounts(supabase, projectIds), { enabled: projectIds.length > 0 });
// -----
const isLoading = isLoadingBusinesses || isLoadingProjects || isLoadingTags || isLoadingInvestments;
const error = businessError || projectError || tagError || investmentError;
const results: Business[] =
businesses?.map((business) => ({
...business,
Projects:
projects
?.filter((project) => project.businessId === business.id)
.map((project) => ({
...project,
tags: tags?.filter((tag) => tag.itemId === project.id).map((tag) => tag.Tag.value) || [],
investmentCount: investmentCounts?.find((ic) => ic.projectId === project.id)?.count || 0,
})) || [],
})) || [];
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error fetching data: {error.message}</p>;
return (
<div>
<div className="mt-10 mx-[15%]">
<h1 className="text-4xl font-bold">Result</h1>
<Separator className="my-4" />
{results.length === 0 && <p>No results found.</p>}
{results.length > 0 && (
<ul>
{results.map((business) => (
<li key={business.id}>
{/* <h2>{business.businessName}</h2>
<p>Joined Date: {new Date(business.joinedDate).toLocaleDateString()}</p>
{business.Projects.map((project) => (
<ExtendableCard
key={project.id}
name={project.projectName}
description={project.projectName}
joinDate={project.projectName}
location={"Bangkok"}
minInvestment={project.ProjectInvestmentDetail[0]?.minInvestment}
totalInvestor={project.ProjectInvestmentDetail[0]?.totalInvestment}
totalRaised={project.ProjectInvestmentDetail[0]?.targetInvestment}
tags={null}
/>
))} */}
<Card className="w-full">
<CardHeader>
<CardTitle>{business.businessName}</CardTitle>
<CardDescription>Joined Date: {new Date(business.joinedDate).toLocaleDateString()}</CardDescription>
</CardHeader>
<CardContent>
{business.Projects.map((project) => (
<ExtendableCard
key={project.id}
name={project.projectName}
description={project.projectName}
joinDate={project.projectName}
location={"Bangkok"}
minInvestment={project.ProjectInvestmentDetail[0]?.minInvestment}
totalInvestor={project.ProjectInvestmentDetail[0]?.totalInvestment}
totalRaised={project.ProjectInvestmentDetail[0]?.targetInvestment}
tags={[]}
/>
))}
</CardContent>
</Card>
</li>
))}
</ul>
)}
<ReactQueryDevtools initialIsOpen={false} />
</div>
</div>
);
}

144
src/app/invest/1/page.tsx Normal file
View File

@ -0,0 +1,144 @@
"use client";
import { Separator } from "@/components/ui/separator";
import { Input } from "@/components/ui/input";
import { CardsPaymentMethod } from "@/components/paymentMethod";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { useRouter } from "next/navigation";
import { toast } from "react-hot-toast";
const term_data = [
{
term: "Minimum Investment",
description: "The minimum investment amount is $500.",
},
{
term: "Investment Horizon",
description: "Investments are typically locked for a minimum of 12 months.",
},
{
term: "Fees",
description: "A management fee of 2% will be applied annually.",
},
{
term: "Returns",
description: "Expected annual returns are between 8% and 12%.",
},
{
term: "Risk Disclosure",
description: "Investments carry risks, including the loss of principal.",
},
{
term: "Withdrawal Policy",
description: "Withdrawals can be made after the lock-in period.",
},
];
export default function Invest() {
const [checkedTerms, setCheckedTerms] = useState(Array(term_data.length).fill(false));
const [error, setError] = useState("");
const router = useRouter(); // Initialize the router
const handleCheckboxChange = (index) => {
const updatedCheckedTerms = [...checkedTerms];
updatedCheckedTerms[index] = !updatedCheckedTerms[index];
setCheckedTerms(updatedCheckedTerms);
};
const handleTermServiceClick = () => {
if (checkedTerms.some((checked) => !checked)) {
setError("Please accept all terms before proceeding with the investment.");
} else {
setError("");
handleInvestmentSuccess();
}
};
const handleInvestmentSuccess = () => {
toast.success("You successfully invested!");
setTimeout(() => {
router.push("/");
}, 1000);
};
return (
<div className="mx-40 my-10">
<h1 className="text-4xl font-bold">Invest on NVIDIA</h1>
<Separator className="my-4" />
<div>
<div className="w-1/2 space-y-2">
<h2 className="text-2xl">Investment Amount</h2>
<Input type="number" placeholder="min $500" />
</div>
<Separator className="my-4" />
<div className="w-1/2 space-y-2">
<h2 className="text-2xl">Payment Information</h2>
<CardsPaymentMethod />
</div>
<Separator className="my-4" />
<div className="w-2/3 space-y-2">
<h2 className="text-2xl">Terms and Services</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Select</TableHead>
<TableHead>Term</TableHead>
<TableHead>Description</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{term_data.map((item, index) => (
<TableRow key={index}>
<TableCell>
<input type="checkbox" checked={checkedTerms[index]} onChange={() => handleCheckboxChange(index)} />
</TableCell>
<TableCell>{item.term}</TableCell>
<TableCell>{item.description}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<Dialog>
<DialogTrigger asChild>
<Button className="mt-4">Invest</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>This action cannot be undone. This will permanently!</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-start">
<Button type="submit" onClick={handleTermServiceClick}>
Confirm
</Button>
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
</DialogFooter>
{error && <p className="text-red-500 mt-2 text-lg font-bold">{error}</p>}
</DialogContent>
</Dialog>
</div>
</div>
);
}

View File

@ -2,13 +2,7 @@
import React, { useState, useEffect } from "react";
import Image from "next/image";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel";
import { Card, CardContent } from "@/components/ui/card";
import CountUp from "react-countup";
import { Progress } from "@/components/ui/progress";
@ -18,12 +12,8 @@ import { ShareIcon, StarIcon } from "lucide-react";
import { Toaster, toast } from "react-hot-toast";
import useSession from "@/lib/supabase/useSession";
import { redirect } from "next/navigation";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import Link from "next/link";
export default function Invest() {
const [progress, setProgress] = useState(0);
@ -69,21 +59,20 @@ export default function Invest() {
<div>
<Toaster position="top-right" reverseOrder={false} />
</div>
<div className="w-[90%] h-[45w0px]-500 m-auto mt-12 pl-24">
<div className="w-[90%] h-[450px]-500 md:m-auto mt-12 md:mt-12 pl-14 md:pl-24">
<div>
<div className="flex ">
<Image src="./logo.svg" alt="logo" width={50} height={50} />
<h1 className="mt-3 font-bold text-3xl">NVIDIA</h1>
<div className="grid grid-cols-2 gap-5 ml-[850px] ">
{/* Name, star and share button packed */}
<div className="grid grid-cols-4">
<div className="flex col-span-2">
<Image src="./logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
<div className="mt-3 font-bold text-lg md:text-3xl">NVIDIA</div>
</div>
<div className="grid grid-cols-2 gap-5 justify-self-end ">
<div className="mt-2 cursor-pointer" onClick={handleFollow}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<StarIcon
id="follow"
fill={isFollow ? "#FFFF00" : "#fff"}
strokeWidth={2}
/>
<StarIcon id="follow" fill={isFollow ? "#FFFF00" : "#fff"} strokeWidth={2} />
</TooltipTrigger>
<TooltipContent>
<p>Follow NIVIDIA</p>
@ -96,104 +85,90 @@ export default function Invest() {
</div>
</div>
</div>
<p className="mt-2"> World's first non-metal sustainable battery</p>
{/* end of pack */}
<p className="mt-2 sm:text-sm"> World's first non-metal sustainable battery</p>
<div className="flex flex-wrap mt-3">
{["Technology", "Gaming"].map((tag) => (
<span
key={tag}
className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1"
>
<span key={tag} className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
{tag}
</span>
))}
</div>
<div className="flex">
<div className="grid grid-cols-2 mt-5">
{/* image carousel */}
<Carousel className="w-[55%] mt-4">
<CarouselContent className="h-[450px]">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<div>
<Carousel className="w-full mt-20 md:mt-0">
<CarouselContent className="h-[400px] flex h-full">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<img src="./boiler1.jpg" alt="" className="rounded-lg self-center" />
</CarouselItem>
))}
</CarouselContent>{" "}
<CarouselPrevious />
<CarouselNext />
</Carousel>
<Carousel className="w-2/3 md:w-full ml-10 md:ml-0 mt-5 md:mt-10 ">
<CarouselContent>
{/* boiler plate for an actual pictures */}
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
))}
</CarouselContent>{" "}
<CarouselPrevious />
<CarouselNext />
</Carousel>
<div className=" w-1/3 mt-4 h-[400px] ml-[8%] ">
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
</CarouselContent>
</Carousel>
</div>
<div className=" w-2/3 mt-4 m-auto grid-rows-5">
<div className="pl-5">
<h1 className="font-semibold text-4xl mt-8">
<CountUp
start={0}
end={100000}
duration={2}
prefix="$"
className=""
/>
<h1 className="font-semibold text-xl md:text-4xl mt-8">
<CountUp start={0} end={100000} duration={2} prefix="$" className="" />
</h1>
<p className=""> 5% raised of $5M max goal</p>
<p className="text-sm md:text-lg"> 5% raised of $5M max goal</p>
<Progress value={progress} className="w-[60%] h-3 mt-3" />
<h1 className="font-semibold text-4xl mt-8">
<h1 className="font-semibold text-4xl md:mt-8">
{" "}
<CountUp start={0} end={1000} duration={2} className="" />
<CountUp start={0} end={1000} duration={2} className="text-xl md:text-4xl" />
</h1>
<p className=""> Investors</p>
<p className="text-sm md:text-lg"> Investors</p>
</div>
<Separator decorative className="mt-3 w-3/4 ml-5" />
<h1 className="font-semibold text-4xl mt-8 ml-5">
{" "}
<CountUp start={0} end={5800} duration={2} className="" /> hours
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5">
<CountUp start={0} end={5800} duration={2} className="text-xl md:text-4xl" /> hours
</h1>
<p className="ml-5"> Left to invest</p>
<Button className="mt-10 ml-[25%]">Invest in NVIDIA</Button>
<Button className="mt-5 md:mt-10 ml-0 md:ml-[25%] scale-75 md:scale-100">
<Link href="/invest/1">Invest in NVIDIA</Link>
</Button>
</div>
</div>
</div>
<Carousel className="w-1/2 ml-10">
<CarouselContent>
{/* boiler plate for an actual pictures */}
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
</CarouselItem>
</CarouselContent>
</Carousel>
</div>
{/* menu */}
<div className="flex w-[90%] mt-24 m-auto ml-32">
<ul className="list-none flex gap-10 text-xl ">
<div className="flex w-[90%] mt-24 m-auto ml-10 md:ml-32">
<ul className="list-none flex gap-10 text-lg md:text-xl ">
<li>
<a
onClick={() => handleClick("Pitch")}
className={tab === "Pitch" ? "text-blue-600" : ""}
>
<a onClick={() => handleClick("Pitch")} className={tab === "Pitch" ? "text-blue-600" : ""}>
Pitch
</a>
</li>
<li>
<a
onClick={() => handleClick("General Data")}
className={tab === "General Data" ? "text-blue-600" : ""}
>
<a onClick={() => handleClick("General Data")} className={tab === "General Data" ? "text-blue-600" : ""}>
General Data
</a>
</li>
<li>
<a
onClick={() => handleClick("Updates")}
className={tab === "Updates" ? "text-blue-600" : ""}
>
<a onClick={() => handleClick("Updates")} className={tab === "Updates" ? "text-blue-600" : ""}>
Updates
</a>
</li>

View File

@ -1,9 +1,11 @@
import type { Metadata } from "next";
import { Montserrat } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { ReactQueryClientProvider } from "@/components/ReactQueryClientProvider";
import "@/app/globals.css";
import { NavigationBar } from "@/components/navigationBar/nav";
import { Toaster } from "react-hot-toast";
const montserrat = Montserrat({
subsets: ["latin"],
@ -24,16 +26,21 @@ interface RootLayoutProps {
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<head />
<body className={`${montserrat.className}`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="relative flex min-h-screen flex-col">
<NavigationBar />
<div className="flex-1 bg-background">{children}</div>
</div>
</ThemeProvider>
</body>
</html>
<ReactQueryClientProvider>
<html lang="en">
<head />
<body className={`${montserrat.className}`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="relative flex min-h-screen flex-col">
<div>
<Toaster position="top-center" reverseOrder={false} toastOptions={{ duration: 1000 }} />
</div>
<NavigationBar />
<div className="flex-1 bg-background">{children}</div>
</div>
</ThemeProvider>
</body>
</html>
</ReactQueryClientProvider>
);
}

View File

@ -11,8 +11,8 @@ export default function Notification() {
];
return (
<div>
<div className="ml-56 mt-16 ">
<h1 className="font-bold text-3xl h-0">Notifications</h1>
<div className="ml-24 md:ml-56 mt-16 ">
<h1 className="font-bold text-2xl md:text-3xl h-0">Notifications</h1>
<div className=" w-full mt-20 ">
{/* Cards */}
<Card className=" border-slate-800 w-3/4 p-6">

View File

@ -1,13 +1,7 @@
import Image from "next/image";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { ExtendableCard } from "@/components/extendableCard";
@ -16,16 +10,14 @@ export default function Home() {
<main>
<div className="flex flex-row bg-slate-100 dark:bg-gray-800">
<div className="flex flex-col w-3/5">
<span className="px-28 py-20">
<p className="text-4xl font-bold">Explore the world of ventures</p>
<span className="text-lg">
<p>
Unlock opportunities and connect with a community of passionate
</p>
<span className="px-10 md:px-28 py-10 md:py-20">
<p className="text-lg md:text-4xl font-bold">Explore the world of ventures</p>
<span className="text-sm md:text-lg">
<p>Unlock opportunities and connect with a community of passionate</p>
<p>investors and innovators.</p>
<p>Together, we turn ideas into impact.</p>
</span>
<Button className="font-bold mt-4">
<Button className="scale-75 md:scale-100 font-bold mt-4">
<Link href="/deals">Start Investing</Link>
</Button>
</span>
@ -42,36 +34,36 @@ export default function Home() {
</div>
</div>
<div className="flex flex-row gap-10 justify-center mt-5">
<div className="flex flex-row gap-0 md:gap-10 justify-start md:justify-center mt-3 md:mt-5">
<Card className="border-0 shadow-none">
<CardHeader>
<CardTitle>100M+</CardTitle>
<CardTitle className="text-lg md:text-2xl">100M+</CardTitle>
<CardDescription>Global investor community</CardDescription>
</CardHeader>
</Card>
<Card className="border-0 shadow-none">
<CardHeader>
<CardTitle>2,500+</CardTitle>
<CardTitle className="text-lg md:text-2xl">2,500+</CardTitle>
<CardDescription>Ventures supported</CardDescription>
</CardHeader>
</Card>
<Card className="border-0 shadow-none">
<CardHeader>
<CardTitle>$2.6B+</CardTitle>
<CardTitle className="text-lg md:text-2xl">$2.6B+</CardTitle>
<CardDescription>Capital raised</CardDescription>
</CardHeader>
</Card>
<Card className="border-0 shadow-none">
<CardHeader className="pb-2">
<CardTitle>Follow Us</CardTitle>
<CardTitle className="text-lg md:text-2xl">Follow Us</CardTitle>
</CardHeader>
<CardContent className="flex gap-2">
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground">
<Image src={"/github.svg"} width={20} height={20} alt="github" />
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
Github
</Button>
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground">
<Image src={"/github.svg"} width={20} height={20} alt="github" />
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
Github
</Button>
</CardContent>
@ -80,23 +72,19 @@ export default function Home() {
<Separator className="mb-6" />
<div className="flex flex-col px-28">
<div className="flex flex-col px-10 md:px-28">
<span className="pb-5">
<p className="text-2xl font-bold">Hottest Deals</p>
<p className="text-lg">
The deals attracting the most interest right now
</p>
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
<p className="text-md md:text-lg">The deals attracting the most interest right now</p>
</span>
<div className="grid grid-cols-4 gap-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Link href={"/invest"}>
<ExtendableCard
name={"NVDA"}
description={
"Founded in 1993, NVIDIA is a key innovator of computer graphics and AI technology"
}
description={"Founded in 1993, NVIDIA is a key innovator of computer graphics and AI technology"}
joinDate={"December 2021"}
location={"Bangkok, Thailand"}
tags={null}
tags={[]}
minInvestment={10000}
totalInvestor={58400}
totalRaised={9000000}
@ -109,7 +97,7 @@ export default function Home() {
}
joinDate={"February 2020"}
location={"Cupertino, California, USA"}
tags={null}
tags={[]}
minInvestment={10000}
totalInvestor={58400}
totalRaised={9000000}
@ -121,25 +109,23 @@ export default function Home() {
}
joinDate={"April 2019"}
location={"Mountain View, California, USA"}
tags={null}
tags={[]}
minInvestment={10000}
totalInvestor={5000}
totalRaised={1500000000}
/>
<ExtendableCard
name={"Microsoft Corporation"}
description={
"Microsoft Corporation is a multinational technology company."
}
description={"Microsoft Corporation is a multinational technology company."}
joinDate={"January 2018"}
location={"California, USA"}
tags={null}
tags={[]}
minInvestment={250}
totalInvestor={5000}
totalRaised={1500000}
/>
</div>
<div className="self-center py-5">
<div className="self-center py-5 scale-75 md:scale-100">
<Button>
<Link href={"/deals"}>View all</Link>
</Button>

View File

@ -0,0 +1,20 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};

View File

@ -0,0 +1,39 @@
"use client";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
export function LoginForm() {
const router = useRouter();
const supabase = createSupabaseClient();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
await supabase.auth.signInWithPassword({
email,
password,
});
router.push("/");
};
return (
<div className="flex flex-col space-y-2">
<Input id="email" type="text" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<Button id="login" onClick={handleLogin}>
Login
</Button>
</div>
);
}

View File

@ -11,7 +11,7 @@ interface ExtendableCardProps {
description: string;
joinDate: string;
location: string;
tags: XMap | null;
tags: XMap | null | never[] | string[];
minInvestment: number;
totalInvestor: number;
totalRaised: number;
@ -19,7 +19,7 @@ interface ExtendableCardProps {
export function ExtendableCard(props: ExtendableCardProps) {
return (
<div className="group relative w-full max-w-sm overflow-hidden rounded-lg bg-card shadow-md transition-all duration-500 hover:shadow-lg">
<div className="group relative w-full max-w-sm overflow-hidden rounded-lg bg-card shadow-md transition-all duration-500 hover:shadow-lg ">
<div className="aspect-[4/3] overflow-hidden">
<img
src="/money.png"
@ -30,27 +30,27 @@ export function ExtendableCard(props: ExtendableCardProps) {
style={{ aspectRatio: "400/300", objectFit: "cover" }}
/>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-card-foreground transition-colors duration-500 group-hover:text-primary">
<div className="p-1 md:p-4">
<div className="text-sm md:text-lg font-semibold text-card-foreground transition-colors duration-500 group-hover:text-primary">
{props.name}
</h3>
</div>
{/* Default content (visible when not hovered) */}
<div className="mt-2 flex items-center text-muted-foreground group-hover:hidden">
<span className="flex items-center pt-2 gap-1">
<CalendarDaysIcon width={20} />
Joined {props.joinDate}
<div className="text-xs md:text-lg">Joined {props.joinDate}</div>
</span>
</div>
<div className="mt-2 flex items-center text-muted-foreground group-hover:hidden">
<span className="text-sm">{props.location}</span>
<span className="text-xs md:text-sm">{props.location}</span>
</div>
<div className="mt-2 flex flex-wrap items-center text-muted-foreground group-hover:hidden">
{["Technology", "Gaming"].map((tag) => (
{props.tags.map((tag) => (
<span
id="tag"
key={tag}
className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1"
>
className="text-[10px] md:text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
{tag}
</span>
))}
@ -58,24 +58,21 @@ export function ExtendableCard(props: ExtendableCardProps) {
{/* Hover content (appears when hovered) */}
<div className="mt-4 max-h-0 overflow-hidden opacity-0 group-hover:max-h-[500px] group-hover:opacity-100 transition-all duration-1000 ease-in-out">
<p className="text-sm text-muted-foreground">{props.description}</p>
<p className="text-xs md:text-sm text-muted-foreground">{props.description}</p>
<div className="mt-4 flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>
<hr className="w-screen -ml-4 mb-2" />
<p>
<strong>${props.totalRaised.toLocaleString()}</strong>{" "}
committed and reserved
<p className="text-xs md:text-base">
<strong>${props.totalRaised.toLocaleString()}</strong> committed and reserved
</p>
<hr className="w-screen -ml-4 mb-2 mt-2" />
<p className="mb-2">
<strong>{props.totalInvestor.toLocaleString()}</strong>{" "}
investors
<p className="mb-2 text-xs md:text-base">
<strong>{props.totalInvestor.toLocaleString()}</strong> investors
</p>
<hr className="w-screen -ml-4 mb-2" />
<p>
<strong>${props.minInvestment.toLocaleString()}</strong> min.
investment
<p className="text-xs md:text-base">
<strong>${props.minInvestment.toLocaleString()}</strong> min. investment
</p>
</div>
</div>

View File

@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { useRouter } from 'next/navigation';
import {
NavigationMenu,
NavigationMenuContent,
@ -133,6 +134,8 @@ export function NavigationBar() {
const { session, loading } = useSession();
const user = session?.user;
const [sessionLoaded, setSessionLoaded] = React.useState(false);
const [searchActive, setSearchActive] = React.useState(false);
const router = useRouter();
React.useEffect(() => {
if (!loading) {
@ -140,6 +143,15 @@ export function NavigationBar() {
}
}, [loading]);
const handleKeyDown = async (k: React.KeyboardEvent<HTMLInputElement>) => {
if (k.key === 'Enter') {
const query = (k.target as HTMLInputElement).value.trim();
if (query) {
router.push(`/find?query=${encodeURIComponent(query)}`);
}
}
};
const businessComponents = [
{
title: "Businesses",
@ -249,8 +261,21 @@ export function NavigationBar() {
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem className="pl-5">
<Search />
<NavigationMenuItem className="pl-5 flex">
<Search
onClick={() => setSearchActive(!searchActive)}
className="cursor-pointer"
/>
{/* search bar's input */}
<input
type="text"
placeholder="Enter business name..."
className={cn(
"ml-2 border rounded-md px-2 py-1 transition-all duration-300 ease-in-out ",
searchActive ? "w-48 opacity-100" : "w-0 opacity-0"
)}
onKeyDown={handleKeyDown}
/>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>

View File

@ -0,0 +1,122 @@
"use client";
import { Icons } from "./ui/icons";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
export function CardsPaymentMethod() {
return (
<Card>
<CardHeader>
<CardTitle>Payment Method</CardTitle>
<CardDescription>Add a new payment method to your account.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<RadioGroup defaultValue="card" className="grid grid-cols-3 gap-4">
<div>
<RadioGroupItem value="card" id="card" className="peer sr-only" aria-label="Card" />
<Label
htmlFor="card"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="mb-3 h-6 w-6">
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
Card
</Label>
</div>
<div>
<RadioGroupItem value="paypal" id="paypal" className="peer sr-only" aria-label="Paypal" />
<Label
htmlFor="paypal"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary">
<Icons.paypal className="mb-3 h-6 w-6" />
Paypal
</Label>
</div>
<div>
<RadioGroupItem value="apple" id="apple" className="peer sr-only" aria-label="Apple" />
<Label
htmlFor="apple"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary">
<Icons.apple className="mb-3 h-6 w-6" />
Apple
</Label>
</div>
</RadioGroup>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="First Last" />
</div>
<div className="grid gap-2">
<Label htmlFor="city">City</Label>
<Input id="city" placeholder="" />
</div>
<div className="grid gap-2">
<Label htmlFor="number">Card number</Label>
<Input id="number" placeholder="" />
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid gap-2">
<Label htmlFor="month">Expires</Label>
<Select>
<SelectTrigger id="month" aria-label="Month">
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">January</SelectItem>
<SelectItem value="2">February</SelectItem>
<SelectItem value="3">March</SelectItem>
<SelectItem value="4">April</SelectItem>
<SelectItem value="5">May</SelectItem>
<SelectItem value="6">June</SelectItem>
<SelectItem value="7">July</SelectItem>
<SelectItem value="8">August</SelectItem>
<SelectItem value="9">September</SelectItem>
<SelectItem value="10">October</SelectItem>
<SelectItem value="11">November</SelectItem>
<SelectItem value="12">December</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="year">Year</Label>
<Select>
<SelectTrigger id="year" aria-label="Year">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 10 }, (_, i) => (
<SelectItem key={i} value={`${new Date().getFullYear() + i}`}>
{new Date().getFullYear() + i}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="cvc">CVC</Label>
<Input id="cvc" placeholder="CVC" />
</div>
</div>
</CardContent>
{/* <CardFooter>
<Button className="w-full">Continue</Button>
</CardFooter> */}
</Card>
);
}

View File

@ -0,0 +1,59 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
const data = [
{
name: "Olivia Martin",
email: "olivia.martin@email.com",
amount: "1900.00",
avatar: "/avatars/01.png", // psuedo avatar image
initials: "OM",
},
{
name: "Jackson Lee",
email: "jackson.lee@email.com",
amount: "39.00",
avatar: "/avatars/02.png",
initials: "JL",
},
{
name: "Isabella Nguyen",
email: "isabella.nguyen@email.com",
amount: "299.00",
avatar: "/avatars/03.png",
initials: "IN",
},
{
name: "William Kim",
email: "will@email.com",
amount: "99.00",
avatar: "/avatars/04.png",
initials: "WK",
},
{
name: "Sofia Davis",
email: "sofia.davis@email.com",
amount: "39.00",
avatar: "/avatars/05.png",
initials: "SD",
},
];
export function RecentFunds() {
return (
<div className="space-y-8">
{data.map((person, index) => (
<div className="flex items-center" key={index}>
<Avatar className="h-9 w-9">
<AvatarImage src={person.avatar} alt={person.name} />
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{person.name}</p>
<p className="text-sm text-muted-foreground">{person.email}</p>
</div>
<div className="ml-auto font-medium">+${person.amount}</div>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

136
src/components/ui/icons.tsx Normal file
View File

@ -0,0 +1,136 @@
type IconProps = React.HTMLAttributes<SVGElement>;
export const Icons = {
logo: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" {...props}>
<rect width="256" height="256" fill="none" />
<line
x1="208"
y1="128"
x2="128"
y2="208"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="16"
/>
<line
x1="192"
y1="40"
x2="40"
y2="192"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="16"
/>
</svg>
),
twitter: (props: IconProps) => (
<svg {...props} height="23" viewBox="0 0 1200 1227" width="23" xmlns="http://www.w3.org/2000/svg">
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" />
</svg>
),
gitHub: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"></path>
</svg>
),
radix: (props: IconProps) => (
<svg viewBox="0 0 25 25" fill="none" {...props}>
<path d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z" fill="currentcolor"></path>
<path d="M12 0H4V8H12V0Z" fill="currentcolor"></path>
<path
d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"
fill="currentcolor"></path>
</svg>
),
aria: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M13.966 22.624l-1.69-4.281H8.122l3.892-9.144 5.662 13.425zM8.884 1.376H0v21.248zm15.116 0h-8.884L24 22.624Z" />
</svg>
),
npm: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
fill="currentColor"
/>
</svg>
),
yarn: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z"
fill="currentColor"
/>
</svg>
),
pnpm: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z"
fill="currentColor"
/>
</svg>
),
react: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
fill="currentColor"
/>
</svg>
),
tailwind: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z"
fill="currentColor"
/>
</svg>
),
google: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
/>
</svg>
),
apple: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
),
paypal: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"
fill="currentColor"
/>
</svg>
),
spinner: (props: IconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
),
};

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,110 @@
"use client";
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, LineChart, Line } from "recharts";
const data = [
{
name: "Jan",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Feb",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Mar",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Apr",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "May",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jun",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jul",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Aug",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Sep",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Oct",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Nov",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Dec",
total: Math.floor(Math.random() * 5000) + 1000,
},
];
interface OverViewProps{
graphType:string;
}
export function Overview(props: OverViewProps) {
return (
<ResponsiveContainer width="100%" height={350}>
{props.graphType === 'line' ? (
<LineChart data={data}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Line
dataKey="total"
fill="currentColor"
className="fill-primary"
/>
</LineChart>
) : (
<BarChart data={data}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Bar
dataKey="total"
fill="currentColor"
className="fill-primary"
/>
</BarChart>
)}
</ResponsiveContainer>
);
}

View File

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

117
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

25
test_util/global-setup.ts Normal file
View File

@ -0,0 +1,25 @@
import { firefox, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const email = process.env.NEXT_PUBLIC_DUMMY_EMAIL;
const password = process.env.NEXT_PUBLIC_DUMMY_PASSWORD;
const baseUrl = 'http://127.0.0.1:3000';
if (!email || !password) {
throw new Error('NEXT_PUBLIC_DUMMY_EMAIL and NEXT_PUBLIC_DUMMY_PASSWORD must be set');
}
const browser = await firefox.launch();
const page = await browser.newPage();
await page.goto(baseUrl + '/auth');
await page.fill('id=email', email);
await page.fill('id=password', password);
await Promise.all([
page.waitForURL(baseUrl),
page.click('id=login')
]);
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
}
export default globalSetup;

23
tests/test-1.spec.ts Normal file
View File

@ -0,0 +1,23 @@
import { test, expect } from '@playwright/test';
test.use({
storageState: './storageState.json'
});
test('Test search businesses', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/');
await page.getByLabel('Main').getByRole('img').click();
const businessInput = page.getByPlaceholder('Enter business name...');
await expect(businessInput).toBeVisible();
await businessInput.fill('neon');
await businessInput.press('Enter');
const heading = page.getByRole('heading', { name: 'Neon Solution, A dummy company' });
await expect(heading).toBeVisible();
await heading.click();
const fundSection = page.locator('div').filter({ hasText: /^Neon raising fund #1$/ });
await expect(fundSection).toBeVisible();
await fundSection.click();
});

30
tests/test-2.spec.ts Normal file
View File

@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test';
test.use({
storageState: './storageState.json'
});
test('Test filter with tags', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/');
await page.getByRole('button', { name: 'Start Investing' }).click();
await page.locator('button').filter({ hasText: 'Tags' }).click();
await page.getByLabel('AI', { exact: true }).click();
await page.locator('span#tag', { hasText: 'AI' });
await page.locator('button').filter({ hasText: 'AI' }).click();
await page.getByLabel('Technology').click();
await page.locator('span#tag', { hasText: 'Technology' });
await page.locator('button').filter({ hasText: 'Technology' }).click();
await page.getByText('Consumer Electronics').click();
await page.locator('span#tag', { hasText: 'Consumer Electronics' });
await page.locator('button').filter({ hasText: 'Consumer Electronics' }).click();
await page.getByLabel('Software').click();
await page.locator('span#tag', { hasText: 'Software' });
await page.locator('button').filter({ hasText: 'Software' }).click();
await page.getByLabel('Internet').click();
await page.locator('span#tag', { hasText: 'Internet' });
});

26
tests/test-3.spec.ts Normal file
View File

@ -0,0 +1,26 @@
import { test, expect } from '@playwright/test';
test.use({
storageState: './storageState.json'
});
test('Test dashboard visibility', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/dashboard');
const dashboardHeading = page.locator('h2', { hasText: 'Dashboard' });
await expect(dashboardHeading).toBeVisible();
const profileViewHeading = page.locator('h3', { hasText: 'Profile Views' });
await expect(profileViewHeading).toBeVisible();
const totalFollowerHeading = page.locator('h3', { hasText: 'Total Followers' });
await expect(totalFollowerHeading).toBeVisible();
const fundsRaisedHeading = page.locator('h3', { hasText: 'Total Funds Raised' });
await expect(fundsRaisedHeading).toBeVisible();
const overviewHeading = page.locator('h3', { hasText: 'Overview' });
await expect(overviewHeading).toBeVisible();
const recentFundHeading = page.locator('h3', { hasText: 'Recent Funds' });
await expect(recentFundHeading).toBeVisible();
});

107
tests/test-4.spec.ts Normal file
View File

@ -0,0 +1,107 @@
import { test, expect, Page } from '@playwright/test';
test.use({
storageState: './storageState.json',
});
test('Investment process test', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/');
// Navigate to the investment page
// await page.getByRole('link', { name: 'Card image NVDA Founded in' }).click();
await page.click('a[href="/invest"]');
await page.getByRole('button', { name: 'Invest in NVIDIA' }).click();
// Fill investment amount
await fillInvestmentAmount(page, '10000');
// Fill card information
await fillCardInformation(page, {
name: 'Dummy',
city: 'Bangkok',
cardNumber: '4111 1111 1111 1111',
expirationMonth: 'August',
expirationYear: '2032',
cvc: '111',
});
// Accept terms
await acceptTerms(page, [
'Minimum Investment',
'Investment Horizon',
'Fees',
'Returns',
]);
// Click Invest button and confirm
await page.getByRole('button', { name: 'Invest' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// Ensure error message is displayed when not all terms are accepted
await ensureErrorMessageDisplayed(page, 'Please accept all terms');
// Close the error dialog
await closeErrorDialog(page);
// Accept remaining terms
await acceptTerms(page, [
'Risk Disclosure',
'Withdrawal Policy',
]);
// Click Invest button and confirm again
await page.getByRole('button', { name: 'Invest' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// Ensure that success toast is displayed when investment is successful
await expect(
page.locator('div[role="status"][aria-live="polite"]').filter({ hasText: /^You successfully invested!$/ })
).toBeVisible();
// Helper functions
async function fillInvestmentAmount(page: Page, amount: string): Promise<void> {
await page.getByPlaceholder('min $').click();
await page.getByPlaceholder('min $').fill(amount);
}
interface CardInfo {
name: string;
city: string;
cardNumber: string;
expirationMonth: string;
expirationYear: string;
cvc: string;
}
async function fillCardInformation(
page: Page,
{ name, city, cardNumber, expirationMonth, expirationYear, cvc }: CardInfo
): Promise<void> {
await page.getByPlaceholder('First Last').click();
await page.getByPlaceholder('First Last').fill(name);
await page.getByLabel('City').click();
await page.getByLabel('City').fill(city);
await page.getByLabel('Card number').click();
await page.getByLabel('Card number').fill(cardNumber);
await page.getByLabel('Month').click();
await page.getByText(expirationMonth).click();
await page.getByLabel('Year').click();
await page.getByLabel(expirationYear).click();
await page.getByPlaceholder('CVC').click();
await page.getByPlaceholder('CVC').fill(cvc);
}
async function acceptTerms(page: Page, terms: string[]): Promise<void> {
for (const term of terms) {
await page.getByRole('row', { name: new RegExp(term) }).getByRole('checkbox').check();
}
}
async function ensureErrorMessageDisplayed(page: Page, message: string): Promise<void> {
await expect(page.getByText(message)).toBeVisible();
}
async function closeErrorDialog(page: Page): Promise<void> {
await page.getByRole('button', { name: 'Close' }).first().click();
}
});