diff options
38 files changed, 953 insertions, 0 deletions
diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/frontend/.github/workflows/ci.yml b/frontend/.github/workflows/ci.yml new file mode 100644 index 0000000..150da5f --- /dev/null +++ b/frontend/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - name: Install + run: bun install --frozen-lockfile + - name: Check formatting + run: bun run prettier-check + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - name: Install + run: bun install --frozen-lockfile + - name: Lint + run: bun run lint diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..b184158 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,176 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +# Created by https://www.toptal.com/developers/gitignore/api/intellij +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/intellij +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode diff --git a/frontend/.gitmodules b/frontend/.gitmodules new file mode 100644 index 0000000..2d6c487 --- /dev/null +++ b/frontend/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/api"] + path = src/api + url = https://github.com/ansg191/ibd-trader-api.git diff --git a/frontend/.idea/codeStyles/Project.xml b/frontend/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..4ae23e4 --- /dev/null +++ b/frontend/.idea/codeStyles/Project.xml @@ -0,0 +1,73 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <AndroidXmlCodeStyleSettings> + <option name="USE_CUSTOM_SETTINGS" value="true" /> + </AndroidXmlCodeStyleSettings> + <HTMLCodeStyleSettings> + <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" /> + </HTMLCodeStyleSettings> + <JSCodeStyleSettings version="0"> + <option name="FORCE_SEMICOLON_STYLE" value="true" /> + <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> + <option name="FORCE_QUOTE_STYlE" value="true" /> + <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" /> + <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> + <option name="SPACES_WITHIN_IMPORTS" value="true" /> + </JSCodeStyleSettings> + <JetCodeStyleSettings> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> + </JetCodeStyleSettings> + <TypeScriptCodeStyleSettings version="0"> + <option name="FORCE_SEMICOLON_STYLE" value="true" /> + <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> + <option name="FORCE_QUOTE_STYlE" value="true" /> + <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" /> + <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> + <option name="SPACES_WITHIN_IMPORTS" value="true" /> + </TypeScriptCodeStyleSettings> + <VueCodeStyleSettings> + <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" /> + <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" /> + </VueCodeStyleSettings> + <codeStyleSettings language="HCL-Terraform"> + <indentOptions> + <option name="TAB_SIZE" value="2" /> + <option name="USE_TAB_CHARACTER" value="true" /> + <option name="SMART_TABS" value="true" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="HTML"> + <option name="SOFT_MARGINS" value="80" /> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="JavaScript"> + <option name="SOFT_MARGINS" value="80" /> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="TypeScript"> + <option name="SOFT_MARGINS" value="80" /> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="Vue"> + <option name="SOFT_MARGINS" value="80" /> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="kotlin"> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> + </codeStyleSettings> + </code_scheme> +</component>
\ No newline at end of file diff --git a/frontend/.idea/codeStyles/codeStyleConfig.xml b/frontend/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/frontend/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="USE_PER_PROJECT_SETTINGS" value="true" /> + </state> +</component>
\ No newline at end of file diff --git a/frontend/.idea/frontend.iml b/frontend/.idea/frontend.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/frontend/.idea/frontend.iml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="WEB_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/.tmp" /> + <excludeFolder url="file://$MODULE_DIR$/temp" /> + <excludeFolder url="file://$MODULE_DIR$/tmp" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file diff --git a/frontend/.idea/inspectionProfiles/Project_Default.xml b/frontend/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/frontend/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> + </profile> +</component>
\ No newline at end of file diff --git a/frontend/.idea/modules.xml b/frontend/.idea/modules.xml new file mode 100644 index 0000000..f3d93d7 --- /dev/null +++ b/frontend/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" /> + </modules> + </component> +</project>
\ No newline at end of file diff --git a/frontend/.idea/vcs.xml b/frontend/.idea/vcs.xml new file mode 100644 index 0000000..dc5671c --- /dev/null +++ b/frontend/.idea/vcs.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/src/api" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..757fd64 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "es5" +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c403366 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/frontend/bun.lockb b/frontend/bun.lockb Binary files differnew file mode 100755 index 0000000..8d89cf8 --- /dev/null +++ b/frontend/bun.lockb diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..79fa2bb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "ibd-trader-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "generate": "make -C src/api generate-js", + "prettier-check": "prettier --check .", + "prettier-write": "prettier --write ." + }, + "dependencies": { + "@auth0/nextjs-auth0": "^3.5.0", + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect-node": "^1.4.0", + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "server-only": "^0.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "postcss": "^8", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..cbee702 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0px" y="0px" viewBox="0 0 895.26 510.7" style="enable-background:new 0 0 895.26 510.7;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#004099;} + .st1{fill:#505050;} +</style> +<g> + <path class="st0" d="M40.17,461.15l36.23-50.95V74.07L40.17,23.13h141.28l-36.25,50.94v336.14l36.25,50.95H40.17z"></path> + <path class="st0" d="M358.27,460.42H210.68l34.14-47.79V70.18l-34.14-47.79h150.74c88.76,0,128.67,51.99,128.67,107.66 c0,55.15-27.84,89.29-62.5,99.27c42.54,9.97,79.83,52.51,79.83,105.03C507.43,413.14,459.12,460.42,358.27,460.42z M359.32,72.81 h-47.79v136.03h39.4c45.16,0,72.48-29.41,72.48-75.11C423.4,102.22,402.91,72.81,359.32,72.81z M369.83,261.88h-58.3v145.49h52.52 c49.36,0,76.14-29.94,76.14-70.9C440.2,293.39,412.36,261.88,369.83,261.88z"></path> + <path class="st0" d="M661.59,458.13H512.96l33.09-49.36V68.95L512.96,20.1h141.81c127.63,0,187.5,80.88,187.5,210.08 C842.27,378.82,779.77,458.13,661.59,458.13z M650.03,72.09H615.9v332.46h39.92c71.95,0,113.97-49.89,113.97-168.06 C769.79,130.4,732.5,72.09,650.03,72.09z"></path> +</g> +<g> + <path class="st1" d="M839.29,76.09c-15.49,0-26.13-10.75-26.13-26.83c0-15.95,10.87-26.94,26.24-26.94 c15.38,0,26.13,10.87,26.13,26.94C865.53,65.23,854.55,76.09,839.29,76.09z M839.4,25.45c-12.6,0-22.54,8.56-22.54,23.82 c0,15.15,10.06,23.7,22.43,23.7c12.6,0,22.54-8.55,22.54-23.7C861.83,34.01,851.77,25.45,839.4,25.45z M847.03,63.61l-8.91-12.72 h-2.89v12.25h-4.97V33.43h8.9c6.25,0,10.41,3.12,10.41,8.67c0,4.28-2.31,7.05-6.01,8.21l8.67,12.14L847.03,63.61z M839.17,38.05 h-3.93v8.78h3.7c3.35,0,5.44-1.38,5.44-4.39C844.37,39.56,842.53,38.05,839.17,38.05z"></path> +</g> +</svg>
\ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..d2f8422 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
\ No newline at end of file diff --git a/frontend/src/api b/frontend/src/api new file mode 160000 +Subproject 250159ea00457090d9cc7b3cc270618d14ce9a9 diff --git a/frontend/src/app/api/auth/[auth0]/route.ts b/frontend/src/app/api/auth/[auth0]/route.ts new file mode 100644 index 0000000..3ce09af --- /dev/null +++ b/frontend/src/app/api/auth/[auth0]/route.ts @@ -0,0 +1,3 @@ +import { handleAuth } from "@auth0/nextjs-auth0"; + +export const GET = handleAuth(); diff --git a/frontend/src/app/api/new-user/add-ibd-creds/route.ts b/frontend/src/app/api/new-user/add-ibd-creds/route.ts new file mode 100644 index 0000000..3740b90 --- /dev/null +++ b/frontend/src/app/api/new-user/add-ibd-creds/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createUserServiceClient } from "@/client/client"; +import { RequestBody, ResponseBody } from "./types"; +import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; + +export const PUT = withApiAuthRequired(async function(req: NextRequest) { + const res = new NextResponse(); + const session = await getSession(req, res); + if (!session || !session.user["sub"]) { + return res; + } + + const { username, password } = RequestBody.parse(await req.json()); + + const client = createUserServiceClient(); + await client.updateUser({ + user: { + subject: session.user.sub, + ibdUsername: username, + ibdPassword: password, + }, + updateMask: { + paths: ["ibd_username", "ibd_password"], + }, + }); + + const ret: ResponseBody = {}; + return NextResponse.json(ret, { status: 200 }); +}); diff --git a/frontend/src/app/api/new-user/add-ibd-creds/types.ts b/frontend/src/app/api/new-user/add-ibd-creds/types.ts new file mode 100644 index 0000000..19b25fc --- /dev/null +++ b/frontend/src/app/api/new-user/add-ibd-creds/types.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const RequestBody = z.object({ + username: z.string(), + password: z.string(), +}); +export const ResponseBody = z.object({}); + +export type RequestBody = z.infer<typeof RequestBody>; +export type ResponseBody = z.infer<typeof ResponseBody>; diff --git a/frontend/src/app/api/new-user/check-ibd-creds/route.ts b/frontend/src/app/api/new-user/check-ibd-creds/route.ts new file mode 100644 index 0000000..837f13a --- /dev/null +++ b/frontend/src/app/api/new-user/check-ibd-creds/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createUserServiceClient } from "@/client/client"; +import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; + +export const PUT = withApiAuthRequired(async function(req: NextRequest) { + const res = new NextResponse(); + const session = await getSession(req, res); + if (!session || !session.user["sub"]) { + return res; + } + + const client = createUserServiceClient(); + const { authenticated } = await client.authenticateUser({ + subject: session.user["sub"], + }); + + if (authenticated) { + return new Response(null, { status: 200 }); + } else { + return new Response(null, { status: 401 }); + } +}); diff --git a/frontend/src/app/api/new-user/verify-username/route.ts b/frontend/src/app/api/new-user/verify-username/route.ts new file mode 100644 index 0000000..b60c76e --- /dev/null +++ b/frontend/src/app/api/new-user/verify-username/route.ts @@ -0,0 +1,14 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createUserServiceClient } from "@/client/client"; +import { RequestBody, ResponseBody } from "@/app/api/new-user/verify-username/types"; +import { withApiAuthRequired } from "@auth0/nextjs-auth0"; + +export const POST = withApiAuthRequired(async function(request: NextRequest) { + const { username } = RequestBody.parse(await request.json()); + + const client = createUserServiceClient(); + const { exists } = await client.checkIBDUsername({ ibdUsername: username }); + + const ret: ResponseBody = { exists }; + return NextResponse.json(ret, { status: 200 }); +}); diff --git a/frontend/src/app/api/new-user/verify-username/types.ts b/frontend/src/app/api/new-user/verify-username/types.ts new file mode 100644 index 0000000..5063da4 --- /dev/null +++ b/frontend/src/app/api/new-user/verify-username/types.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const RequestBody = z.object({ + username: z.string(), +}); +export const ResponseBody = z.object({ + exists: z.boolean(), +}); + +export type RequestBody = z.infer<typeof RequestBody>; +export type ResponseBody = z.infer<typeof ResponseBody>; diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..289dcab --- /dev/null +++ b/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,34 @@ +import { Metadata } from "next"; +import { createUserServiceClient } from "@/client/client"; +import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0"; +import Layout from "@/components/Layout/Layout"; +import { redirect, RedirectType } from "next/navigation"; +import { red } from "next/dist/lib/picocolors"; + +export const metadata: Metadata = { + title: "IBD Trader Dashboard", +}; + +export default withPageAuthRequired(async function Dashboard() { + const client = createUserServiceClient(); + + const user = await getSession(); + if (!user) { + redirect("/api/auth/login", RedirectType.replace); + } + + const dbUser = await client.createUser({ + subject: user.user["sub"], + }); + + // Check if user has IBD credentials + if (!dbUser.user || !dbUser.user.ibdUsername) { + redirect("/new-user", RedirectType.replace); + } + + return ( + <Layout> + <h1>Dashboard</h1> + </Layout> + ); +}, { returnTo: "/dashboard" }); diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico Binary files differnew file mode 100644 index 0000000..718d6fe --- /dev/null +++ b/frontend/src/app/favicon.ico diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..39a9271 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..bb15d68 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { UserProvider } from "@auth0/nextjs-auth0/client"; +import { config } from "@fortawesome/fontawesome-svg-core"; +import "@fortawesome/fontawesome-svg-core/styles.css"; + +config.autoAddCss = false; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, + }: Readonly<{ children: React.ReactNode }>) { + return ( + <html lang="en"> + <UserProvider> + <body className={inter.className}>{children}</body> + </UserProvider> + </html> + ); +} diff --git a/frontend/src/app/new-user/page.tsx b/frontend/src/app/new-user/page.tsx new file mode 100644 index 0000000..57fcf82 --- /dev/null +++ b/frontend/src/app/new-user/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from "next"; +import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { redirect, RedirectType } from "next/navigation"; +import { createUserServiceClient } from "@/client/client"; +import NewUserForm from "@/components/NewUserForm/NewUserForm"; + +export const metadata: Metadata = { + title: "New User", +}; + +export default withPageAuthRequired(async function NewUser() { + const session = await getSession(); + if (!session) { + redirect("/api/auth/login", RedirectType.replace); + } + + const client = createUserServiceClient(); + const { user } = await client.getUser({ subject: session.user["sub"] }); + if (!user) { + throw new Error("User not found"); + } + + if (user.ibdUsername) { + // User already has IBD credentials + redirect("/dashboard", RedirectType.replace); + } + + return <NewUserForm />; +}); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..13777a9 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; +import { Metadata } from "next"; +import Link from "next/link"; +import { getSession } from "@auth0/nextjs-auth0"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "IBD Trader", +}; + +export default async function Home() { + const user = await getSession(); + + if (user) { + redirect("/dashboard"); + } + + return ( + <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100"> + <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center"> + <div className="flex justify-center"> + <Image src="/logo.svg" alt="IBD Trader" width={200} height={200} /> + </div> + <h1 className="text-6xl font-bold mt-4">Trader</h1> + <button className="mt-16 px-6 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600"> + <Link href="/api/auth/login">Login</Link> + </button> + </main> + </div> + ); +} diff --git a/frontend/src/client/client.ts b/frontend/src/client/client.ts new file mode 100644 index 0000000..7a92917 --- /dev/null +++ b/frontend/src/client/client.ts @@ -0,0 +1,23 @@ +import "server-only"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { createPromiseClient, PromiseClient } from "@connectrpc/connect"; +import { UserService } from "@/api/gen/idb/user/v1/user_connect"; +import { StockService } from "@/api/gen/idb/stock/v1/stock_connect"; + +const baseUrl = process.env.BACKEND_URL || "http://localhost:8000"; +const transport = createGrpcTransport({ + baseUrl, + httpVersion: "2", +}); + +export type UserServiceClient = PromiseClient<typeof UserService>; + +export function createUserServiceClient(): UserServiceClient { + return createPromiseClient(UserService, transport); +} + +export type StockServiceClient = PromiseClient<typeof StockService> + +export function createStockServiceClient(): StockServiceClient { + return createPromiseClient(StockService, transport); +} diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..6617038 --- /dev/null +++ b/frontend/src/components/Layout/Layout.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React, { useState } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const paths = [ + { href: "/dashboard", label: "Home" }, + { href: "/dashboard/positions", label: "Positions" }, +]; + +export type LayoutProps = Readonly<{ + children: React.ReactNode; +}>; + +export default function Layout({ children }: LayoutProps) { + const [isOpen, setIsOpen] = useState(true); + const path = usePathname(); + + return ( + <div className="flex h-screen bg-gray-100"> + {/* Sidebar */} + <div + className={`relative text-black space-y-6 py-7 px-2 transition-all duration-500 ease-in-out ${isOpen ? "w-64" : "w-12 overflow-hidden"}`}> + {/* Sidebar content */} + <nav className={isOpen ? "" : "hidden"}> + {paths.map(({ href, label }) => ( + <Link key={href} href={href} + className={`block py-2.5 px-4 mb-2 rounded transition duration-200 ${href == path ? "bg-blue-200" : "hover:bg-blue-200"}`}> + {label} + </Link> + ))} + </nav> + {/* Toggle button */} + <div className="absolute bottom-2 right-0 p-2"> + <button onClick={() => setIsOpen(!isOpen)} + className="text-gray-700 focus:outline-none transition-transform duration-500 transform" + style={{ transform: isOpen ? "rotate(180deg)" : "rotate(0)" }} + > + <FontAwesomeIcon icon={faArrowRight} size="2x" /> + </button> + </div> + </div> + + {/* Main content */} + <div className="flex-1 flex flex-col overflow-hidden"> + <main className="flex-1 overflow-y-auto p-4"> + {children} + </main> + </div> + </div> + ); +} diff --git a/frontend/src/components/NewUserForm/NewUserForm.tsx b/frontend/src/components/NewUserForm/NewUserForm.tsx new file mode 100644 index 0000000..779a49f --- /dev/null +++ b/frontend/src/components/NewUserForm/NewUserForm.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { User } from "@/api/gen/idb/user/v1/user_pb"; +import { ChangeEventHandler, FormEventHandler, MouseEventHandler, useEffect, useState } from "react"; +import { + RequestBody as VerifyRequestBody, + ResponseBody as VerifyResponseBody, +} from "@/app/api/new-user/verify-username/types"; +import { + RequestBody as SubmitRequestBody, + ResponseBody as SubmitResponseBody, +} from "@/app/api/new-user/add-ibd-creds/types"; + +import styles from "./styles.module.css"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; + +export default function NewUserForm() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isUsernameValid, setIsUsernameValid] = useState(false); + const [isSubmittingUsername, setIsSubmittingUsername] = useState(false); + const [isSubmittingForm, setIsSubmittingForm] = useState(false); + const [isCheckingCreds, setIsCheckingCreds] = useState(false); + const [error, setError] = useState<string | null>(null); + + const handleUsernameSubmit: FormEventHandler<HTMLFormElement> = (e) => { + e.preventDefault(); + setIsSubmittingUsername(true); + setError(null); + + const body: VerifyRequestBody = { username }; + fetch("/api/new-user/verify-username", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }).then((res) => res.json()) + .then((data) => { + const { exists } = VerifyResponseBody.parse(data); + if (exists) { + setIsUsernameValid(true); + } else { + setError("Username does not exist"); + } + setIsSubmittingUsername(false); + }); + }; + const handleFormSubmit: FormEventHandler<HTMLFormElement> = (e) => { + e.preventDefault(); + setIsSubmittingForm(true); + setError(null); + + const body: SubmitRequestBody = { username, password }; + fetch("/api/new-user/add-ibd-creds", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }).then((res) => res.json()) + .then((data) => { + const {} = SubmitResponseBody.parse(data); + setIsSubmittingForm(false); + setIsCheckingCreds(true); + + return fetch("/api/new-user/check-ibd-creds", { + method: "PUT", + }); + }) + .then((res) => { + if (res.status === 200) { + window.location.href = "/dashboard"; + } else { + setError("Invalid credentials"); + setIsCheckingCreds(false); + } + }); + }; + + const handleBack: MouseEventHandler<HTMLButtonElement> = () => { + setIsUsernameValid(false); + setPassword(""); + setError(null); + }; + + return ( + <div className="flex items-center justify-center min-h-screen bg-gray-100"> + <div className="w-full max-w-md p-8 space-y-4 bg-white rounded-lg shadow-lg"> + <h1 className="text-2xl font-bold text-center">Set Up Your Account</h1> + + <form onSubmit={isUsernameValid ? handleFormSubmit : handleUsernameSubmit}> + <div className="mb-4"> + <label htmlFor="username" className="block text-sm font-medium text-gray-700"> + Username + </label> + <input + type="text" + id="username" + value={username} + onChange={(e) => setUsername(e.target.value)} + className={`w-full px-3 py-2 mt-1 border ${ + isUsernameValid ? "border-gray-300 bg-gray-200" : "border-gray-300" + } rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500`} + required + disabled={isUsernameValid} + /> + {error && !isUsernameValid && ( + <p className="mt-2 text-sm text-red-600">{error}</p> + )} + </div> + {!isUsernameValid && ( + <div className="flex justify-center"> + <button + type="submit" + className="px-4 py-2 text-white bg-indigo-600 rounded-md shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + disabled={isSubmittingUsername} + > + {isSubmittingUsername ? <FontAwesomeIcon icon={faSpinner} spin /> : "Next"} + </button> + </div> + )} + </form> + + {isUsernameValid && ( + <form onSubmit={handleFormSubmit}> + <div className={styles.passwordForm}> + <div className="mb-4"> + <label htmlFor="password" className="block text-sm font-medium text-gray-700"> + Password + </label> + <input + type="password" + id="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + className="w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" + required + /> + </div> + {error && isUsernameValid && ( + <p className="mt-2 text-sm text-red-600">{error}</p> + )} + <div className="flex justify-between"> + <button + type="button" + onClick={handleBack} + className="px-4 py-2 text-white bg-gray-600 rounded-md shadow-sm hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2" + > + Back + </button> + <button + type="submit" + className="px-4 py-2 text-white bg-indigo-600 rounded-md shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + disabled={isSubmittingForm} + > + {isSubmittingForm ? ( + <p><FontAwesomeIcon icon={faSpinner} spin /> Setting Credentials...</p> + ) : isCheckingCreds ? ( + <p><FontAwesomeIcon icon={faSpinner} spin /> Checking Credentials...</p> + ) : "Submit"} + </button> + </div> + </div> + </form> + )} + </div> + </div> + ); +} diff --git a/frontend/src/components/NewUserForm/styles.module.css b/frontend/src/components/NewUserForm/styles.module.css new file mode 100644 index 0000000..a2ce597 --- /dev/null +++ b/frontend/src/components/NewUserForm/styles.module.css @@ -0,0 +1,13 @@ +@keyframes smooth-appear { + to { + opacity: 1; + transform: translateY(0); + } +} + +.passwordForm { + margin-top: 1rem; + transform: translateY(-5rem); + opacity: 0; + animation: smooth-appear 1s ease forwards; +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..e9a0944 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} |