From fd5dcf5756623d351de3073918babf0b0b151a81 Mon Sep 17 00:00:00 2001 From: Noah Spannbauer Date: Mon, 17 Feb 2025 07:52:50 -0600 Subject: [PATCH] adding project page --- .gitignore | 33 +- .mattrbld/collections/profile.json | 19 - .mattrbld/collections/projects.json | 19 - .mattrbld/config.json | 90 -- .mattrbld/schemas/Profile.json | 905 ------------------ .mattrbld/schemas/Projects.json | 214 ----- .../users/noahspannbauer-icloud-com.json | 6 - Dockerfile | 14 + api/.funcignore | 7 - api/host.json | 3 - api/local.settings.json | 13 - api/main/function.json | 17 - api/main/index.ts | 7 - api/main/sample.dat | 3 - api/package.json | 3 +- api/proxies.json | 4 - api/src/app.module.ts | 4 +- api/src/github/github-content.interface.ts | 20 - api/src/github/github-repo.interface.ts | 120 --- api/src/github/github.controller.ts | 35 - api/src/github/github.module.ts | 18 - api/src/github/github.service.ts | 39 - api/src/main.azure.ts | 26 - api/src/main.ts | 18 + api/src/project/project.controller.ts | 92 ++ api/src/project/project.dto.ts | 6 + api/src/project/project.entity.ts | 7 + api/src/project/project.module.ts | 27 + api/src/project/project.service.ts | 38 + app/src/App.css | 42 - app/src/App.tsx | 35 - app/src/components/siteNav/SiteNav.tsx | 140 --- app/src/index.css | 68 -- app/src/main.tsx | 13 - app/src/vite-env.d.ts | 1 - app/vite.config.ts | 7 - {app => app2}/.gitignore | 0 {app => app2}/README.md | 0 {app => app2}/eslint.config.js | 0 {app => app2}/index.html | 2 +- {app => app2}/package.json | 7 +- app2/public/noahspan-logo.png | Bin 0 -> 22165 bytes {app => app2}/public/vite.svg | 0 app2/src/App.tsx | 25 + {app => app2}/src/assets/react.svg | 0 app2/src/auth/msalConfig.ts | 70 ++ app2/src/components/actionMenu/ActionMenu.tsx | 61 ++ .../actionMenu/ActionMenuProps.interface.tsx | 7 + .../confirmationDialog/ConfirmationDialog.tsx | 51 + .../ConfirmationDialogProps.interface.ts | 8 + app2/src/components/profile/Profile.tsx | 108 +++ .../components/projectForm/ProjectForm.tsx | 354 +++++++ .../projectForm/ProjectFormProps.interface.ts | 8 + .../projectForm/ProjectFormState.interface.ts | 5 + app2/src/components/projectForm/reducer.ts | 41 + .../components/projects/Project.interface.ts | 11 + app2/src/components/projects/Projects.tsx | 248 +++++ .../projects/ProjectsState.interface.ts | 13 + app2/src/components/projects/reducer.ts | 92 ++ app2/src/components/siteNav/SiteNav.tsx | 130 +++ .../src/context/appContext/AppContext.tsx | 0 .../appContext/AppContextProps.interface.ts | 0 .../context/appContext/AppContextProvider.tsx | 0 .../AppContextProviderProps.interface.ts | 0 .../appContext/AppContextState.interface.ts | 0 .../src/context/appContext/reducer.ts | 0 app2/src/enums/formMode.ts | 6 + app2/src/hooks/accessToken/UseAccessToken.tsx | 32 + app2/src/hooks/appContext/UseAppContext.tsx | 11 + .../src/hooks/httpClient/UseHttpClient.tsx | 0 .../Projects.tsx => app2/src/index.css | 0 app2/src/main.tsx | 39 + app2/src/vite-env.d.ts | 12 + {app => app2}/tsconfig.app.json | 0 {app => app2}/tsconfig.json | 0 {app => app2}/tsconfig.node.json | 0 app2/vite.config.ts | 16 + docker-compose.yaml | 11 + infrastructure/.gitignore | 4 + infrastructure/app_config.tf | 42 +- infrastructure/config.tf | 4 + infrastructure/config/main.tf | 52 + infrastructure/config/outputs.tf | 39 + infrastructure/config/variables.tf | 1 + infrastructure/dns.tf | 68 +- infrastructure/main.tf | 220 ++++- .../modules/static_web_app/outputs.tf | 18 +- .../modules/static_web_app/variables.tf | 24 +- infrastructure/outputs.tf | 14 +- infrastructure/resource_group.tf | 3 +- infrastructure/storage_account.tf | 12 + infrastructure/variables.tf | 39 +- pnpm-lock.yaml | 199 ++-- pnpm-workspace.yaml | 3 +- 94 files changed, 2097 insertions(+), 2126 deletions(-) delete mode 100644 .mattrbld/collections/profile.json delete mode 100644 .mattrbld/collections/projects.json delete mode 100644 .mattrbld/config.json delete mode 100644 .mattrbld/schemas/Profile.json delete mode 100644 .mattrbld/schemas/Projects.json delete mode 100644 .mattrbld/users/noahspannbauer-icloud-com.json create mode 100644 Dockerfile delete mode 100644 api/.funcignore delete mode 100644 api/host.json delete mode 100644 api/local.settings.json delete mode 100644 api/main/function.json delete mode 100644 api/main/index.ts delete mode 100644 api/main/sample.dat delete mode 100644 api/proxies.json delete mode 100644 api/src/github/github-content.interface.ts delete mode 100644 api/src/github/github-repo.interface.ts delete mode 100644 api/src/github/github.controller.ts delete mode 100644 api/src/github/github.module.ts delete mode 100644 api/src/github/github.service.ts delete mode 100644 api/src/main.azure.ts create mode 100644 api/src/project/project.controller.ts create mode 100644 api/src/project/project.dto.ts create mode 100644 api/src/project/project.entity.ts create mode 100644 api/src/project/project.module.ts create mode 100644 api/src/project/project.service.ts delete mode 100644 app/src/App.css delete mode 100644 app/src/App.tsx delete mode 100644 app/src/components/siteNav/SiteNav.tsx delete mode 100644 app/src/index.css delete mode 100644 app/src/main.tsx delete mode 100644 app/src/vite-env.d.ts delete mode 100644 app/vite.config.ts rename {app => app2}/.gitignore (100%) rename {app => app2}/README.md (100%) rename {app => app2}/eslint.config.js (100%) rename {app => app2}/index.html (89%) rename {app => app2}/package.json (89%) create mode 100644 app2/public/noahspan-logo.png rename {app => app2}/public/vite.svg (100%) create mode 100644 app2/src/App.tsx rename {app => app2}/src/assets/react.svg (100%) create mode 100644 app2/src/auth/msalConfig.ts create mode 100644 app2/src/components/actionMenu/ActionMenu.tsx create mode 100644 app2/src/components/actionMenu/ActionMenuProps.interface.tsx create mode 100644 app2/src/components/confirmationDialog/ConfirmationDialog.tsx create mode 100644 app2/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts create mode 100644 app2/src/components/profile/Profile.tsx create mode 100644 app2/src/components/projectForm/ProjectForm.tsx create mode 100644 app2/src/components/projectForm/ProjectFormProps.interface.ts create mode 100644 app2/src/components/projectForm/ProjectFormState.interface.ts create mode 100644 app2/src/components/projectForm/reducer.ts create mode 100644 app2/src/components/projects/Project.interface.ts create mode 100644 app2/src/components/projects/Projects.tsx create mode 100644 app2/src/components/projects/ProjectsState.interface.ts create mode 100644 app2/src/components/projects/reducer.ts create mode 100644 app2/src/components/siteNav/SiteNav.tsx rename {app => app2}/src/context/appContext/AppContext.tsx (100%) rename {app => app2}/src/context/appContext/AppContextProps.interface.ts (100%) rename {app => app2}/src/context/appContext/AppContextProvider.tsx (100%) rename {app => app2}/src/context/appContext/AppContextProviderProps.interface.ts (100%) rename {app => app2}/src/context/appContext/AppContextState.interface.ts (100%) rename {app => app2}/src/context/appContext/reducer.ts (100%) create mode 100644 app2/src/enums/formMode.ts create mode 100644 app2/src/hooks/accessToken/UseAccessToken.tsx create mode 100644 app2/src/hooks/appContext/UseAppContext.tsx rename {app => app2}/src/hooks/httpClient/UseHttpClient.tsx (100%) rename app/src/projects/Projects.tsx => app2/src/index.css (100%) create mode 100644 app2/src/main.tsx create mode 100644 app2/src/vite-env.d.ts rename {app => app2}/tsconfig.app.json (100%) rename {app => app2}/tsconfig.json (100%) rename {app => app2}/tsconfig.node.json (100%) create mode 100644 app2/vite.config.ts create mode 100644 docker-compose.yaml create mode 100644 infrastructure/.gitignore create mode 100644 infrastructure/config.tf create mode 100644 infrastructure/config/main.tf create mode 100644 infrastructure/config/outputs.tf create mode 100644 infrastructure/config/variables.tf create mode 100644 infrastructure/storage_account.tf diff --git a/.gitignore b/.gitignore index a5d7b00..18d4021 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,8 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files .vscode/* -!.vscode/extensions.json -.idea +node_modules +**/.env +**/.env.* .DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# Terraform -**/.terraform/* -*.tfstate -*.tfstate.* \ No newline at end of file +.dapr +**/**/secrets.json +.prod \ No newline at end of file diff --git a/.mattrbld/collections/profile.json b/.mattrbld/collections/profile.json deleted file mode 100644 index f49abf5..0000000 --- a/.mattrbld/collections/profile.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "allowedTypes": null, - "dir": "/data/profile", - "disableComments": false, - "disablePreview": false, - "draftByDefault": false, - "schemas": [ - "/.mattrbld/schemas/Profile.json" - ], - "linkable": false, - "maxSize": null, - "permissions": { - "everybody": [ - "everything" - ] - }, - "type": "json", - "urlTemplate": "" -} \ No newline at end of file diff --git a/.mattrbld/collections/projects.json b/.mattrbld/collections/projects.json deleted file mode 100644 index 43f5909..0000000 --- a/.mattrbld/collections/projects.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "allowedTypes": null, - "dir": "/data/projects", - "disableComments": false, - "disablePreview": false, - "draftByDefault": false, - "schemas": [ - "/.mattrbld/schemas/Projects.json" - ], - "linkable": false, - "maxSize": null, - "permissions": { - "everybody": [ - "everything" - ] - }, - "type": "json", - "urlTemplate": "" -} \ No newline at end of file diff --git a/.mattrbld/config.json b/.mattrbld/config.json deleted file mode 100644 index b74dac6..0000000 --- a/.mattrbld/config.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "autoquotes": null, - "brandColors": [], - "corsProxy": "http://localhost:9999", - "name": "noahspan-root", - "sidebar": [ - { - "label": "The sidebar has not yet been configured for this project" - }, - { - "icon": "wrench-and-driver", - "label": "Configure now", - "target": { - "name": "Project.Settings", - "query": { - "tab": "sidebar" - } - }, - "onlyPrivileged": true - }, - { - "separator": true - }, - { - "label": "Dashboard", - "icon": "grid", - "target": { - "name": "Project" - }, - "protected": true - }, - { - "label": "Media Library", - "icon": "image-stack", - "target": { - "name": "Project.MediaLibrary" - }, - "protected": true - }, - { - "label": "Settings", - "icon": "settings", - "target": { - "name": "Project.Settings" - }, - "onlyPrivileged": true, - "protected": true - }, - { - "separator": true - }, - { - "icon": "user", - "label": "Profile", - "target": { - "name": "Project.Collection", - "params": { - "path": "/.mattrbld/collections/profile.json" - } - } - }, - { - "icon": "repo", - "label": "Projects", - "target": { - "name": "Project.Collection", - "params": { - "path": "/.mattrbld/collections/projects.json" - } - } - } - ], - "slugifyOptions": null, - "customRoles": [], - "draftsDir": null, - "previewUrl": null, - "languages": [], - "media": { - "advanced": false, - "customFields": null, - "dir": null, - "maxSize": null, - "outputPath": null, - "permissions": { - "everybody": [ - "everything" - ] - } - } -} \ No newline at end of file diff --git a/.mattrbld/schemas/Profile.json b/.mattrbld/schemas/Profile.json deleted file mode 100644 index ab71b4b..0000000 --- a/.mattrbld/schemas/Profile.json +++ /dev/null @@ -1,905 +0,0 @@ -{ - "fields": [ - { - "type": "rows", - "default": null, - "icon": "rows", - "key": "experiences", - "label": "Experiences", - "options": { - "compact": true, - "allowEditing": true, - "itemLabel": "Row" - }, - "tab": "Experiences", - "validation": { - "max": null, - "min": null, - "unit": "rows" - }, - "value": [ - { - "type": "group", - "displayField": null, - "icon": "group", - "key": "experience", - "label": "Experience", - "tab": "Experiences", - "value": [ - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "jobTitle", - "label": "JobTitle", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Experiences", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "company", - "label": "Company", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Experiences", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "location", - "label": "Location", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Experiences", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "startDate", - "label": "StartDate", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Experiences", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "endDate", - "label": "EndDate", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Experiences", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rich text", - "default": null, - "icon": "text", - "key": "details", - "label": "Details", - "localised": false, - "options": { - "outputFormat": "markdown", - "blockFormats": [ - "blockquote", - "heading", - "hr", - "orderedList", - "unorderedList", - "image" - ], - "inlineFormats": [ - "em", - "strong", - "link" - ], - "formatOptions": [ - "allowQuoteFooters", - "allowNestedLists", - "allowImageCaptions" - ], - "inputRuleOptions": [ - "autoquotes", - "dashes", - "ellipsis", - "noDoubleCaps", - "noDoubleSpace" - ], - "minHeading": 2, - "maxHeading": 4, - "codeLangs": [ - "html", - "css", - "javascript", - "markdown" - ], - "allowRaw": false, - "urlTemplate": "", - "urlSuffix": "", - "linkOptions": [ - "forceBlankTarget", - "allowExternal", - "allowInternal" - ] - }, - "tab": "Experiences", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rows", - "default": null, - "icon": "rows", - "key": "education", - "label": "Education", - "options": { - "compact": true, - "allowEditing": true, - "itemLabel": "Row" - }, - "tab": "Education", - "validation": { - "max": null, - "min": null, - "unit": "rows" - }, - "value": [ - { - "type": "group", - "displayField": null, - "icon": "group", - "key": "Education", - "label": "Education", - "tab": "Education", - "value": [ - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "school", - "label": "School", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Education", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "location", - "label": "Location", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Education", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "degree", - "label": "Degree", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Education", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "fieldOfStudy", - "label": "Field of Study", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Education", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rich text", - "default": null, - "icon": "text", - "key": "details", - "label": "Details", - "localised": false, - "options": { - "outputFormat": "markdown", - "blockFormats": [ - "blockquote", - "heading", - "hr", - "orderedList", - "unorderedList", - "image" - ], - "inlineFormats": [ - "em", - "strong", - "link" - ], - "formatOptions": [ - "allowQuoteFooters", - "allowNestedLists", - "allowImageCaptions" - ], - "inputRuleOptions": [ - "autoquotes", - "dashes", - "ellipsis", - "noDoubleCaps", - "noDoubleSpace" - ], - "minHeading": 2, - "maxHeading": 4, - "codeLangs": [ - "html", - "css", - "javascript", - "markdown" - ], - "allowRaw": false, - "urlTemplate": "", - "urlSuffix": "", - "linkOptions": [ - "forceBlankTarget", - "allowExternal", - "allowInternal" - ] - }, - "tab": "About", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "firstName", - "label": "First Name", - "localised": false, - "options": { - "wrapping": false, - "multiline": false - }, - "tab": "Intro", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "lastName", - "label": "Last Name", - "localised": false, - "options": { - "wrapping": false, - "multiline": false - }, - "tab": "Intro", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "link", - "default": null, - "icon": "link", - "key": "photoUrl", - "label": "Photo URL", - "localised": false, - "options": { - "type": "external", - "byFilePath": false, - "urlSuffix": "/", - "urlTemplate": "" - }, - "tab": "Intro", - "validation": { - "required": false - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "title", - "label": "Title", - "localised": false, - "options": { - "wrapping": false, - "multiline": false - }, - "tab": "Intro", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "location", - "label": "Location", - "localised": false, - "options": { - "wrapping": false, - "multiline": false - }, - "tab": "Intro", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rows", - "default": null, - "icon": "rows", - "key": "skills", - "label": "Skills", - "options": { - "compact": true, - "allowEditing": true, - "itemLabel": "Row" - }, - "tab": "Skills", - "validation": { - "max": null, - "min": null, - "unit": "rows" - }, - "value": [ - { - "type": "group", - "displayField": null, - "icon": "group", - "key": "skillGroup", - "label": "Skill Group", - "tab": "Skills", - "value": [ - { - "type": "select", - "default": null, - "icon": "dropdown", - "key": "category", - "label": "Category", - "options": { - "filterable": false, - "removable": false, - "placeholder": "Select a value…", - "options": [ - "Experienced", - "Familiar" - ] - }, - "tab": "Skills", - "validation": { - "required": false - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "select", - "default": null, - "icon": "dropdown", - "key": "group", - "label": "Group", - "options": { - "filterable": false, - "removable": false, - "placeholder": "Select a value…", - "options": [ - "Agile Methodologies", - "Cloud", - "Containerization", - "Continuous Integration and Continuous Delivery", - "Database", - "Front-End Web Development", - "Infrastructure as Code", - "Messaging", - "Programming Languages", - "Test Automation" - ] - }, - "tab": "Skills", - "validation": { - "required": false - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rows", - "default": null, - "icon": "rows", - "key": "skills", - "label": "Skill", - "options": { - "compact": true, - "allowEditing": true, - "itemLabel": "Row" - }, - "tab": "Skills", - "validation": { - "max": null, - "min": null, - "unit": "rows" - }, - "value": [ - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "skill", - "label": "Skill", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Skills", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rich text", - "default": null, - "icon": "text", - "key": "summary", - "label": "Summary", - "localised": false, - "options": { - "outputFormat": "markdown", - "blockFormats": [ - "blockquote", - "heading", - "hr", - "orderedList", - "unorderedList", - "image" - ], - "inlineFormats": [ - "em", - "strong", - "link" - ], - "formatOptions": [ - "allowQuoteFooters", - "allowNestedLists", - "allowImageCaptions" - ], - "inputRuleOptions": [ - "autoquotes", - "dashes", - "ellipsis", - "noDoubleCaps", - "noDoubleSpace" - ], - "minHeading": 2, - "maxHeading": 4, - "codeLangs": [ - "html", - "css", - "javascript", - "markdown" - ], - "allowRaw": false, - "urlTemplate": "", - "urlSuffix": "", - "linkOptions": [ - "forceBlankTarget", - "allowExternal", - "allowInternal" - ] - }, - "tab": "About", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "tabs": [ - { - "label": "Intro", - "groupAs": "intro" - }, - { - "label": "About", - "groupAs": "about" - }, - { - "label": "Skills", - "groupAs": null - }, - { - "label": "Experiences", - "groupAs": null - }, - { - "label": "Education", - "groupAs": null - } - ] -} \ No newline at end of file diff --git a/.mattrbld/schemas/Projects.json b/.mattrbld/schemas/Projects.json deleted file mode 100644 index 55f3505..0000000 --- a/.mattrbld/schemas/Projects.json +++ /dev/null @@ -1,214 +0,0 @@ -{ - "fields": [ - { - "type": "rows", - "default": null, - "icon": "rows", - "key": "projects", - "label": "Projects", - "options": { - "compact": true, - "allowEditing": true, - "itemLabel": "Row" - }, - "tab": "Untitled Tab", - "validation": { - "max": null, - "min": null, - "unit": "rows" - }, - "value": [ - { - "type": "group", - "displayField": null, - "icon": "group", - "key": "project", - "label": "Project", - "tab": "Untitled Tab", - "value": [ - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "repoName", - "label": "Repo Name", - "localised": false, - "options": { - "wrapping": false, - "multiline": false - }, - "tab": "Untitled Tab", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "title", - "label": "Title", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Untitled Tab", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "text", - "default": null, - "icon": "text-input", - "key": "iconName", - "label": "Icon Name", - "localised": false, - "options": { - "wrapping": true, - "multiline": false - }, - "tab": "Untitled Tab", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - }, - { - "type": "rich text", - "default": null, - "icon": "text", - "key": "summary", - "label": "Summary", - "localised": false, - "options": { - "outputFormat": "markdown", - "blockFormats": [ - "blockquote", - "heading", - "hr", - "orderedList", - "unorderedList", - "image" - ], - "inlineFormats": [ - "em", - "strong", - "link" - ], - "formatOptions": [ - "allowQuoteFooters", - "allowNestedLists", - "allowImageCaptions" - ], - "inputRuleOptions": [ - "autoquotes", - "dashes", - "ellipsis", - "noDoubleCaps", - "noDoubleSpace" - ], - "minHeading": 2, - "maxHeading": 4, - "codeLangs": [ - "html", - "css", - "javascript", - "markdown" - ], - "allowRaw": false, - "urlTemplate": "", - "urlSuffix": "", - "linkOptions": [ - "forceBlankTarget", - "allowExternal", - "allowInternal" - ] - }, - "tab": "Untitled Tab", - "validation": { - "enforceMinMax": true, - "max": null, - "min": null, - "regex": null, - "regexError": null, - "required": false, - "unit": "length" - }, - "value": null, - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "version": 1, - "visibility": { - "hidden": false, - "showByValue": { - "field": null - } - } - } - ], - "tabs": [ - { - "label": "Untitled Tab", - "groupAs": null - } - ] -} \ No newline at end of file diff --git a/.mattrbld/users/noahspannbauer-icloud-com.json b/.mattrbld/users/noahspannbauer-icloud-com.json deleted file mode 100644 index bb16983..0000000 --- a/.mattrbld/users/noahspannbauer-icloud-com.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "noahspannbauer@icloud.com", - "id": "noahspannbauer-icloud-com", - "name": "Noah", - "role": "owner" -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9c065b5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:18-slim AS base + +FROM base AS api +WORKDIR api +COPY ./.prod/api . +EXPOSE 3000 +CMD ["node", "dist/main.js"] + +FROM base AS app +WORKDIR /app +COPY ./.prod/app . +RUN npm i -g serve +EXPOSE 8080 +CMD [ "serve", "-s", "dist", "-p", "8080" ] \ No newline at end of file diff --git a/api/.funcignore b/api/.funcignore deleted file mode 100644 index 5179222..0000000 --- a/api/.funcignore +++ /dev/null @@ -1,7 +0,0 @@ -*.js.map -*.ts -.git* -.vscode -local.settings.json -test -tsconfig.json \ No newline at end of file diff --git a/api/host.json b/api/host.json deleted file mode 100644 index d2059a4..0000000 --- a/api/host.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "version": "2.0" -} diff --git a/api/local.settings.json b/api/local.settings.json deleted file mode 100644 index 3603ddd..0000000 --- a/api/local.settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "", - "AZURE_STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://localhost:10002/devstoreaccount1;", - "FUNCTIONS_WORKER_RUNTIME": "node", - "GITHUB_API_USERNAME": "noahspannbauer", - "GITHUB_API_PAT": "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" - }, - "Host": { - "CORS": "*" - } -} diff --git a/api/main/function.json b/api/main/function.json deleted file mode 100644 index 1e5c89e..0000000 --- a/api/main/function.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "{*segments}" - }, - { - "type": "http", - "direction": "out", - "name": "res" - } - ], - "scriptFile": "../dist/main/index.js" -} diff --git a/api/main/index.ts b/api/main/index.ts deleted file mode 100644 index 71f010c..0000000 --- a/api/main/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Context, HttpRequest } from '@azure/functions'; -import { AzureHttpAdapter } from '@nestjs/azure-func-http'; -import { createApp } from '../src/main.azure'; - -export default function(context: Context, req: HttpRequest): void { - AzureHttpAdapter.handle(createApp, context, req); -} diff --git a/api/main/sample.dat b/api/main/sample.dat deleted file mode 100644 index 26aac46..0000000 --- a/api/main/sample.dat +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Azure" -} \ No newline at end of file diff --git a/api/package.json b/api/package.json index 7037a2f..81f9157 100644 --- a/api/package.json +++ b/api/package.json @@ -17,8 +17,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", - "start:azure": "npm run build && func host start" + "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { "@azure/functions": "^1.0.3", diff --git a/api/proxies.json b/api/proxies.json deleted file mode 100644 index b385252..0000000 --- a/api/proxies.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/proxies", - "proxies": {} -} diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 115ceb1..0e4d247 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'; import { GitHubApiModule } from './github/github.module'; import { APP_FILTER } from '@nestjs/core'; import { HttpExceptionFilter } from './filters/http-exception.filter'; +import { ProjectModule } from './project/project.module'; @Module({ imports: [ - GitHubApiModule + GitHubApiModule, + ProjectModule ], providers: [ { diff --git a/api/src/github/github-content.interface.ts b/api/src/github/github-content.interface.ts deleted file mode 100644 index e62b49e..0000000 --- a/api/src/github/github-content.interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface Content { - name: string - path: string - sha: string - size: number - url: string - html_url: string - git_url: string - download_url: string - type: string - content: string - encoding: string - _links: Links -} - -interface Links { - self: string - git: string - html: string -} \ No newline at end of file diff --git a/api/src/github/github-repo.interface.ts b/api/src/github/github-repo.interface.ts deleted file mode 100644 index 2e56c11..0000000 --- a/api/src/github/github-repo.interface.ts +++ /dev/null @@ -1,120 +0,0 @@ -export interface Repo { - id: number - node_id: string - name: string - full_name: string - private: boolean - owner: Owner - html_url: string - description?: string - fork: boolean - url: string - forks_url: string - keys_url: string - collaborators_url: string - teams_url: string - hooks_url: string - issue_events_url: string - events_url: string - assignees_url: string - branches_url: string - tags_url: string - blobs_url: string - git_tags_url: string - git_refs_url: string - trees_url: string - statuses_url: string - languages_url: string - stargazers_url: string - contributors_url: string - subscribers_url: string - subscription_url: string - commits_url: string - git_commits_url: string - comments_url: string - issue_comment_url: string - contents_url: string - compare_url: string - merges_url: string - archive_url: string - downloads_url: string - issues_url: string - pulls_url: string - milestones_url: string - notifications_url: string - labels_url: string - releases_url: string - deployments_url: string - created_at: string - updated_at: string - pushed_at: string - git_url: string - ssh_url: string - clone_url: string - svn_url: string - homepage?: string - size: number - stargazers_count: number - watchers_count: number - language?: string - has_issues: boolean - has_projects: boolean - has_downloads: boolean - has_wiki: boolean - has_pages: boolean - has_discussions: boolean - forks_count: number - mirror_url: any - archived: boolean - disabled: boolean - open_issues_count: number - license?: License - allow_forking: boolean - is_template: boolean - web_commit_signoff_required: boolean - topics: any[] - visibility: string - forks: number - open_issues: number - watchers: number - default_branch: string - permissions: Permissions -} - -interface Owner { - login: string - id: number - node_id: string - avatar_url: string - gravatar_id: string - url: string - html_url: string - followers_url: string - following_url: string - gists_url: string - starred_url: string - subscriptions_url: string - organizations_url: string - repos_url: string - events_url: string - received_events_url: string - type: string - user_view_type: string - site_admin: boolean -} - -interface License { - key: string - name: string - spdx_id: string - url: string - node_id: string -} - -interface Permissions { - admin: boolean - maintain: boolean - push: boolean - triage: boolean - pull: boolean -} diff --git a/api/src/github/github.controller.ts b/api/src/github/github.controller.ts deleted file mode 100644 index 40721d1..0000000 --- a/api/src/github/github.controller.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Controller, Get, HttpException, Query } from '@nestjs/common'; -import { GitHubApiService } from './github.service'; -import { AxiosError } from 'axios' -import { Content } from './github-content.interface'; -import { Repo } from './github-repo.interface'; - -@Controller('github') -export class GitHubApiController { - constructor(private readonly githubApiService: GitHubApiService) {} - - @Get('readme') - async getReadMe(@Query() query: any): Promise { - try { - const repoName = query['repoName']; - - return await this.githubApiService.getReadMeContent(repoName) - } catch (error) { - const axiosError = error as AxiosError; - - throw new HttpException(axiosError.message, axiosError.status) - } - } - - @Get('repos') - async getRepos(): Promise { - try { - return await this.githubApiService.getRepos(); - } catch (error) { - const axiosError = error as AxiosError - - throw new HttpException(axiosError.message, axiosError.status) - } - } -} - diff --git a/api/src/github/github.module.ts b/api/src/github/github.module.ts deleted file mode 100644 index f050b17..0000000 --- a/api/src/github/github.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GitHubApiController } from './github.controller'; -import { GitHubApiService } from './github.service'; -import { ConfigModule } from '@nestjs/config'; -import configuration from '../config/configuration'; -import { HttpModule } from '@nestjs/axios'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - load: [configuration] - }), - HttpModule - ], - controllers: [GitHubApiController], - providers: [GitHubApiService], -}) -export class GitHubApiModule {} diff --git a/api/src/github/github.service.ts b/api/src/github/github.service.ts deleted file mode 100644 index 8c68fa0..0000000 --- a/api/src/github/github.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; -import { AxiosResponse } from 'axios'; -import { Repo } from './github-repo.interface'; -import { Content } from './github-content.interface'; - -@Injectable() -export class GitHubApiService { - constructor( - private readonly configService: ConfigService, - private readonly httpService: HttpService - ) {} - - async getReadMeContent(repoName: string): Promise { - const githubApiUsername = this.configService.get('githubApiUsername') - const response: AxiosResponse = await this.httpService.axiosRef.get(`https://api.github.com/repos/${githubApiUsername}/${repoName}/contents/README.md`, { - auth: { - username: this.configService.get('githubApiUsername'), - password: this.configService.get('githubApiPassword') - } - }) - const content: Content = response.data - - return content; - } - - async getRepos(): Promise { - const response: AxiosResponse = await this.httpService.axiosRef.get('https://api.github.com/user/repos', { - auth: { - username: this.configService.get('githubApiUsername'), - password: this.configService.get('githubApiPassword') - } - }) - const repos: Repo[] = response.data - - return repos; - } -} diff --git a/api/src/main.azure.ts b/api/src/main.azure.ts deleted file mode 100644 index b0a02ed..0000000 --- a/api/src/main.azure.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { INestApplication, InternalServerErrorException } from '@nestjs/common'; -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { HttpService } from '@nestjs/axios' -import { HttpExceptionFilter } from './filters/http-exception.filter'; - -export async function createApp(): Promise { - const httpService = new HttpService(); - const app = await NestFactory.create(AppModule); - - app.setGlobalPrefix('api'); - app.useGlobalFilters(new HttpExceptionFilter()); - httpService.axiosRef.interceptors.response.use( - (response) => { - return response; - }, - (error) => { - console.error('Internal server error exception', error); - - throw new InternalServerErrorException(); - } - ); - - await app.init(); - return app; -} diff --git a/api/src/main.ts b/api/src/main.ts index 3541ee6..50361e2 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,8 +1,26 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { HttpService } from '@nestjs/axios'; +import { HttpExceptionFilter } from './filters/http-exception.filter'; +import { InternalServerErrorException } from '@nestjs/common'; async function bootstrap() { + const httpService = new HttpService(); const app = await NestFactory.create(AppModule); + + app.enableCors(); + app.setGlobalPrefix('api'); + app.useGlobalFilters(new HttpExceptionFilter()); + httpService.axiosRef.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + console.error('Internal server error exception', error); + + throw new InternalServerErrorException(); + } + ); await app.listen(3000); } diff --git a/api/src/project/project.controller.ts b/api/src/project/project.controller.ts new file mode 100644 index 0000000..13d8b15 --- /dev/null +++ b/api/src/project/project.controller.ts @@ -0,0 +1,92 @@ +import { + Body, + Controller, + Delete, + Get, + HttpException, + Param, + Post, + Put +} from '@nestjs/common'; +import { ProjectDto } from './project.dto'; +import { Project } from './project.entity'; +import { ProjectService } from './project.service'; +import { CustomError } from '@noahspan/noahspan-modules'; + +@Controller('projects') +export class ProjectController { + constructor(private readonly projectService: ProjectService) {} + + @Get(':partitionKey/:rowKey') + async find( + @Param('partitionKey') partitionKey: string, + @Param('rowKey') rowKey: string + ) { + try { + return await this.projectService.find(partitionKey, rowKey); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Get() + async findAll() { + try { + return await this.projectService.findAll(); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Post() + async create(@Body() projectDto: ProjectDto) { + try { + const project = new Project(); + + Object.assign(project, projectDto); + + return await this.projectService.create(project); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Put(':partitionKey/:rowKey') + async update( + @Param('partitionKey') partitionKey: string, + @Param('rowKey') rowKey: string, + @Body() projectDto: ProjectDto + ) { + try { + const project = new Project(); + + Object.assign(project, projectDto); + + return await this.projectService.update(partitionKey, rowKey, project); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Delete(':partitionKey/:rowKey') + async delete( + @Param('partitionKey') partitionKey: string, + @Param('rowKey') rowKey: string + ) { + try { + return await this.projectService.delete(partitionKey, rowKey); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } +} diff --git a/api/src/project/project.dto.ts b/api/src/project/project.dto.ts new file mode 100644 index 0000000..e2ad22d --- /dev/null +++ b/api/src/project/project.dto.ts @@ -0,0 +1,6 @@ +export class ProjectDto { + rowKey: string; + displayName: string; + icon: string; + summary: string; +} \ No newline at end of file diff --git a/api/src/project/project.entity.ts b/api/src/project/project.entity.ts new file mode 100644 index 0000000..d22b3f3 --- /dev/null +++ b/api/src/project/project.entity.ts @@ -0,0 +1,7 @@ +export class Project { + partitionKey: string; + rowKey: string; + displayName: string; + icon: string; + summary: string; +} \ No newline at end of file diff --git a/api/src/project/project.module.ts b/api/src/project/project.module.ts new file mode 100644 index 0000000..0fb7515 --- /dev/null +++ b/api/src/project/project.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { ProjectController } from './project.controller'; +import { ProjectService } from './project.service'; +import { AzureTableStorageModule } from '@noahspan/azure-database'; +import { Project } from './project.entity'; +import { ConfigModule, ConfigService } from '@nestjs/config'; + +@Module({ + imports: [ + AzureTableStorageModule.forRootAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => { + return { + connectionString: configService.get('azureStorageConnectionString') + }; + }, + inject: [ConfigService] + }), + AzureTableStorageModule.forFeature(Project, { + createTableIfNotExists: true, + table: 'projects' + }), + ], + controllers: [ProjectController], + providers: [ProjectService] +}) +export class ProjectModule {} diff --git a/api/src/project/project.service.ts b/api/src/project/project.service.ts new file mode 100644 index 0000000..334888d --- /dev/null +++ b/api/src/project/project.service.ts @@ -0,0 +1,38 @@ +import { InjectRepository, Repository } from '@noahspan/azure-database'; +import { Injectable } from '@nestjs/common'; +import { Project } from './project.entity'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class ProjectService { + constructor( + @InjectRepository(Project) + private readonly projectRepository: Repository + ) {} + + async find(partitionKey: string, rowKey: string): Promise { + return await this.projectRepository.find(partitionKey, rowKey); + } + + async findAll(): Promise { + return await this.projectRepository.findAll(); + } + + async create(project: Project): Promise { + project.partitionKey = 'project'; + + return await this.projectRepository.create(project); + } + + async update( + partitionKey: string, + rowKey: string, + project: Project + ): Promise { + return await this.projectRepository.update(partitionKey, rowKey, project); + } + + async delete(partitionKey: string, rowKey: string): Promise { + await this.projectRepository.delete(partitionKey, rowKey); + } +} diff --git a/app/src/App.css b/app/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/app/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/app/src/App.tsx b/app/src/App.tsx deleted file mode 100644 index 3d7ded3..0000000 --- a/app/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/app/src/components/siteNav/SiteNav.tsx b/app/src/components/siteNav/SiteNav.tsx deleted file mode 100644 index 0c892d6..0000000 --- a/app/src/components/siteNav/SiteNav.tsx +++ /dev/null @@ -1,140 +0,0 @@ -// import { useEffect, useState } from 'react'; -// import { ISiteNavProps } from './ISiteNavProps'; -// import { useNavigate } from 'react-router-dom'; -// import { useAppContext } from '../../hooks/appContext/UseAppContext'; -// import { -// Icon, -// MenuItem, -// Navbar, -// SignOutIcon, -// Spinner, -// Typography -// } from '@noahspan/noahspan-components'; -// import { useIsAuthenticated, useMsal } from '@azure/msal-react'; -// import { InteractionStatus } from '@azure/msal-browser'; -// import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; -// import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; -// import { AxiosInstance, AxiosResponse } from 'axios'; -// import { User } from '@microsoft/microsoft-graph-types'; -// import { EventMessage, EventPayload, EventType } from '@azure/msal-browser'; - -// type EventPayloadExtended = EventPayload & { accessToken: string }; - -// const SiteNav: React.FC = () => { -// const [loading, setLoading] = useState(false); -// const [userPhoto, setUserPhoto] = useState(); -// const httpClient: AxiosInstance = useHttpClient(); -// const appContext = useAppContext(); -// const isAuthenticated = useIsAuthenticated(); -// const { getAccessToken } = useAccessToken(); -// const { instance } = useMsal(); -// const navigate = useNavigate(); -// const pages = [ -// { -// name: 'Logbook', -// url: '/' -// }, -// { -// name: 'Pilots', -// url: '/pilots' -// } -// ]; -// const handleSignIn = () => { -// instance.loginRedirect({ -// scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`] -// }); -// }; -// const handleSignOut = () => { -// instance.logoutRedirect(); -// }; -// const getUserProfile = async (accessToken: string): Promise => { -// try { -// const response: AxiosResponse = await httpClient.get(`api/userProfile`, { -// headers: { -// Authorization: accessToken -// } -// }); -// const userProfile: User = response.data; - -// return userProfile; -// } catch (error) { -// throw new Error(); -// } -// }; -// const getUserPhoto = async (accessToken: string): Promise => { -// try { -// const response: AxiosResponse = await httpClient.get(`api/userPhoto`, { -// headers: { -// Authorization: accessToken -// }, -// responseType: 'arraybuffer' -// }); -// const arrayBufferView = new Uint8Array(response.data); -// const blob = new Blob([arrayBufferView], { type: 'image/png' }); -// const imageUrl = window.URL.createObjectURL(blob); - -// return imageUrl; -// } catch (error) { -// console.log(error); -// throw new Error(); -// } -// }; -// const handlePageClick = (url: string) => { -// navigate(url); -// }; - -// const Settings = () => { -// return ( -// -// -// -// Sign Out -// -// -// ); -// }; - -// useEffect(() => { -// const setUserProfile = async () => { -// try { -// setLoading(true); - -// const accessToken: string = await getAccessToken(); -// const userProfile = await getUserProfile(accessToken); -// const userPhoto = await getUserPhoto(accessToken); - -// setUserPhoto(userPhoto); - -// appContext.dispatch({ -// type: 'SET_USER_PROFILE', -// payload: userProfile -// }); -// } catch (error) { -// console.log(error); -// } finally { -// setLoading(false); -// } -// }; - -// if ( -// isAuthenticated && -// Object.keys(appContext.state.userProfile).length === 0 -// ) { -// setUserProfile(); -// } -// }, [isAuthenticated]); - -// return ( -// } -// pages={pages} -// settings={} -// userPhoto={userPhoto} -// /> -// ); -// }; - -// export default SiteNav;å \ No newline at end of file diff --git a/app/src/index.css b/app/src/index.css deleted file mode 100644 index 6119ad9..0000000 --- a/app/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/app/src/main.tsx b/app/src/main.tsx deleted file mode 100644 index 32938e4..0000000 --- a/app/src/main.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' -import { BrowserRouter } from 'react-router-dom' - -createRoot(document.getElementById('root')!).render( - - - - - , -) diff --git a/app/src/vite-env.d.ts b/app/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/app/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/app/vite.config.ts b/app/vite.config.ts deleted file mode 100644 index 8b0f57b..0000000 --- a/app/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}) diff --git a/app/.gitignore b/app2/.gitignore similarity index 100% rename from app/.gitignore rename to app2/.gitignore diff --git a/app/README.md b/app2/README.md similarity index 100% rename from app/README.md rename to app2/README.md diff --git a/app/eslint.config.js b/app2/eslint.config.js similarity index 100% rename from app/eslint.config.js rename to app2/eslint.config.js diff --git a/app/index.html b/app2/index.html similarity index 89% rename from app/index.html rename to app2/index.html index e4b78ea..77fc50f 100644 --- a/app/index.html +++ b/app2/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Noah Spannbauer
diff --git a/app/package.json b/app2/package.json similarity index 89% rename from app/package.json rename to app2/package.json index 630be3c..a319b4d 100644 --- a/app/package.json +++ b/app2/package.json @@ -1,5 +1,5 @@ { - "name": "app", + "name": "app2", "private": true, "version": "0.0.0", "type": "module", @@ -12,15 +12,16 @@ "dependencies": { "@azure/msal-browser": "^4.2.1", "@azure/msal-react": "^3.0.4", - "@noahspan/noahspan-components": "^1.0.0", + "@microsoft/microsoft-graph-types": "^2.40.0", + "@noahspan/noahspan-components": "^1.3.1", "axios": "^1.7.9", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", "react-router-dom": "^7.1.5" }, "devDependencies": { "@eslint/js": "^9.19.0", - "@microsoft/microsoft-graph-types": "^2.40.0", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", diff --git a/app2/public/noahspan-logo.png b/app2/public/noahspan-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c4cce0cf0dd0fe5e6747d0c8daaf61e06311dc14 GIT binary patch literal 22165 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 JP3#(R>ffeIOH=>= literal 0 HcmV?d00001 diff --git a/app/public/vite.svg b/app2/public/vite.svg similarity index 100% rename from app/public/vite.svg rename to app2/public/vite.svg diff --git a/app2/src/App.tsx b/app2/src/App.tsx new file mode 100644 index 0000000..a351b1b --- /dev/null +++ b/app2/src/App.tsx @@ -0,0 +1,25 @@ +import { Route, Routes } from 'react-router-dom'; +import Projects from './components/projects/Projects'; +import SiteNav from './components/siteNav/SiteNav'; +import { useAppContext } from './hooks/appContext/UseAppContext'; +import { IPublicClientApplication } from '@azure/msal-browser'; +import { MsalProvider } from '@azure/msal-react'; + +interface AppProps { + pca: IPublicClientApplication +} + +const App = ({ pca }: AppProps) => { + const appContext = useAppContext(); + + return ( + + + + } /> + + + ) +} + +export default App diff --git a/app/src/assets/react.svg b/app2/src/assets/react.svg similarity index 100% rename from app/src/assets/react.svg rename to app2/src/assets/react.svg diff --git a/app2/src/auth/msalConfig.ts b/app2/src/auth/msalConfig.ts new file mode 100644 index 0000000..24c7b80 --- /dev/null +++ b/app2/src/auth/msalConfig.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { LogLevel } from '@azure/msal-browser'; + +/** + * Configuration object to be passed to MSAL instance on creation. + * For a full list of MSAL.js configuration parameters, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md + */ + +export const msalConfig = { + auth: { + clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply. + authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain + redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration. + postLogoutRedirectUri: '/', // Indicates the page to navigate after logout. + navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response. + }, + cache: { + cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs. + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + }, + system: { + loggerOptions: { + loggerCallback: (level: any, message: any, containsPii: any) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.error(message); + return; + case LogLevel.Info: + console.info(message); + return; + case LogLevel.Verbose: + console.debug(message); + return; + case LogLevel.Warning: + console.warn(message); + return; + default: + return; + } + }, + }, + }, +}; + +/** + * Scopes you add here will be prompted for user consent during sign-in. + * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. + * For more information about OIDC scopes, visit: + * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + */ +export const loginRequest = { + scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`], +}; + +/** + * An optional silentRequest object can be used to achieve silent SSO + * between applications by providing a "login_hint" property. + */ +// export const silentRequest = { +// scopes: ["openid", "profile"], +// loginHint: "example@domain.net" +// }; \ No newline at end of file diff --git a/app2/src/components/actionMenu/ActionMenu.tsx b/app2/src/components/actionMenu/ActionMenu.tsx new file mode 100644 index 0000000..c411075 --- /dev/null +++ b/app2/src/components/actionMenu/ActionMenu.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { ActionMenuProps } from './ActionMenuProps.interface'; +import { + Icon, + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, +} from '@noahspan/noahspan-components'; +import { FormMode } from '../../enums/formMode'; + +const ActionMenu = ({ id, onDelete, onOpenCloseForm }: ActionMenuProps) => { + const [anchorElAction, setAnchorElAction] = useState( + null + ); + + const onOpenActionMenu = (event: React.MouseEvent) => { + setAnchorElAction(event.currentTarget); + }; + + const onCloseActionMenu = () => { + setAnchorElAction(null); + }; + + return ( +
+ + + + + onOpenCloseForm(FormMode.EDIT, id)}> + + + + Edit + + onOpenCloseForm(FormMode.VIEW, id)}> + + + + View + +
+ onDelete(id)}> + + + + Delete + +
+
+ ); +}; + +export default ActionMenu; diff --git a/app2/src/components/actionMenu/ActionMenuProps.interface.tsx b/app2/src/components/actionMenu/ActionMenuProps.interface.tsx new file mode 100644 index 0000000..98309a4 --- /dev/null +++ b/app2/src/components/actionMenu/ActionMenuProps.interface.tsx @@ -0,0 +1,7 @@ +import { FormMode } from '../../enums/formMode'; + +export interface ActionMenuProps { + id: string; + onDelete: (entryId: string) => void; + onOpenCloseForm: (formMode: FormMode, id: string) => void; +} diff --git a/app2/src/components/confirmationDialog/ConfirmationDialog.tsx b/app2/src/components/confirmationDialog/ConfirmationDialog.tsx new file mode 100644 index 0000000..cf0b008 --- /dev/null +++ b/app2/src/components/confirmationDialog/ConfirmationDialog.tsx @@ -0,0 +1,51 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Icon, + Spinner +} from '@noahspan/noahspan-components'; +import { ConfirmationDialogProps } from './ConfirmationDialogProps.interface'; + +const ConfirmationDialog = ({ + contentText, + isLoading, + isOpen, + onCancel, + onConfirm, + title +}: ConfirmationDialogProps) => { + return ( + + {title} + + {!isLoading && {contentText}} + {isLoading && } + + + + + + + ); +}; + +export default ConfirmationDialog; diff --git a/app2/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts b/app2/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts new file mode 100644 index 0000000..af258d3 --- /dev/null +++ b/app2/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts @@ -0,0 +1,8 @@ +export interface ConfirmationDialogProps { + contentText: string; + isLoading: boolean; + isOpen: boolean; + onCancel: () => void; + onConfirm: () => void; + title: string; +} diff --git a/app2/src/components/profile/Profile.tsx b/app2/src/components/profile/Profile.tsx new file mode 100644 index 0000000..f533aa0 --- /dev/null +++ b/app2/src/components/profile/Profile.tsx @@ -0,0 +1,108 @@ +// import { +// Box, +// Container, +// Grid, +// Typography +// } from '@noahspan/noahspan-components'; +// import profile from '../../../data/profile/profile.json'; + +// const getSkills = (category: string) => { +// let skillsString: string = ''; + +// for (const skill of profile.skills) { +// console.log(skill) +// if (skill.category === category) { +// skillsString += `${skill.group} (` +// console.log(skillsString) +// for (const skillName of skill.skills) { +// skillsString += `${skillName}, ` +// } + +// skillsString += '), '; +// } +// } +// console.log(skillsString) +// return skillsString +// } + +// const Profile = () => { +// return ( +// +// +// +// +// {`${profile.intro.firstName} ${profile.intro.lastName}`} +// +// +// {profile.intro.title} +// +// +// {profile.intro.location} +// +// +// Summary +// +// +// {profile.about.details} +// +// +// Skills +// +// +// Experienced +// +// +// {getSkills('Experienced')} +// +// +// Familiar +// +// +// {getSkills('Familiar')} +// +// +// Experience +// +// {profile.experiences.length > 0 && profile.experiences.map((experience) => { +// return ( +// <> +// +// {experience.jobTitle} +// +// +// {`${experience.startDate}-${experience.endDate}`} +// +// +// {experience.company} +// +// +// {experience.location} +// +// +// ) +// })} +// {profile.experiences.length > 0 && profile.education.map((education) => { +// return ( +// <> +// +// {education.school} +// +// +// {education.location} +// +// +// {education.degree} +// +// +// {education.fieldOfStudy} +// +// +// ) +// })} +// +// +// +// ) +// } + +// export default Profile; \ No newline at end of file diff --git a/app2/src/components/projectForm/ProjectForm.tsx b/app2/src/components/projectForm/ProjectForm.tsx new file mode 100644 index 0000000..32a700f --- /dev/null +++ b/app2/src/components/projectForm/ProjectForm.tsx @@ -0,0 +1,354 @@ +import { useEffect, useReducer } from 'react'; +import { + Alert, + Button, + Drawer, + Grid, + Icon, + IconButton, + IconName, + TextField, + Typography +} from '@noahspan/noahspan-components'; +import { useForm, Controller, FormProvider } from 'react-hook-form'; +import { initialState, reducer } from './reducer'; +import { ProjectFormProps } from './ProjectFormProps.interface'; +import { FormMode } from '../../enums/formMode'; +import { useIsAuthenticated } from '@azure/msal-react'; +import { useAccessToken } from '../../hooks/accessToken/UseAccessToken'; +import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; +import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; + +const ProjectForm = ({ isDrawerOpen, mode, onOpenClose, projectId }: ProjectFormProps) => { + const [state, dispatch] = useReducer(reducer, initialState); + const httpClient: AxiosInstance = useHttpClient(); + const { getAccessToken } = useAccessToken(); + const isAuthenticated = useIsAuthenticated(); + const defaultValues = { + partitionKey: 'project', + rowKey: '', + title: '', + subTitle: '', + iconName: '', + summary: '', + repoName: '', + siteUrl: '', + order: '' + } + const methods = useForm({ + defaultValues: defaultValues + }); + const watchRepoName = methods.watch(['repoName']) + + const onCancel = () => { + methods.reset(defaultValues); + dispatch({ type: 'SET_IS_DISABLED', payload: false }); + onOpenClose(FormMode.CANCEL); + }; + + const onSubmit = async (data: unknown) => { + console.log(data) + try { + dispatch({ type: 'SET_IS_LOADING', payload: true }); + + const accessToken: string = await getAccessToken(); + + if (!projectId) { + await httpClient.post(`api/projects`, data, { + headers: { + Authorization: accessToken + } + }); + } else { + await httpClient.put(`api/projects/project/${projectId}`, data, { + headers: { + Authorization: accessToken + } + }); + } + + methods.reset(defaultValues); + dispatch({ type: 'SET_IS_DISABLED', payload: false }); + onOpenClose(FormMode.CANCEL); + } catch (error) { + const axiosError = error as AxiosError; + + dispatch({ type: 'SET_ERROR', payload: axiosError.message }); + } finally { + dispatch({ type: 'SET_IS_LOADING', payload: false }); + } + }; + + useEffect(() => { + if (watchRepoName.length > 0) { + methods.setValue('rowKey', watchRepoName[0]) + } + }, [watchRepoName]) + + useEffect(() => { + if (mode === FormMode.VIEW) { + dispatch({ type: 'SET_IS_DISABLED', payload: true }); + } + }, [mode]); + + useEffect(() => { + const getProject = async () => { + try { + dispatch({ type: 'SET_IS_LOADING', payload: true }); + + const config = isAuthenticated + ? { headers: { Authorization: await getAccessToken() } } + : {}; + const response: AxiosResponse = await httpClient.get( + `api/projects/project/${projectId}`, + config + ); + const entry = response.data; + + methods.reset(entry); + } catch (error) { + const axiosError = error as AxiosError; + + dispatch({ type: 'SET_ERROR', payload: axiosError.message }); + } finally { + dispatch({ type: 'SET_IS_LOADING', payload: false }); + } + }; + + if (projectId && isDrawerOpen) { + getProject(); + } + }, [projectId]); + + return ( + + +
+ + + {`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Project`} + + + + + + + {state.error && ( + + + dispatch({ type: 'SET_ERROR', payload: undefined }) + } + severity="error" + sx={{ width: '100%' }} + > + {state.error} + + + )} + + Order + + + ( + + )} + /> + + + Title + + + ( + + )} + /> + + + Sub Title + + + ( + + )} + /> + + + Icon Name + + + ( + + )} + /> + + + Repo Name + + + ( + + )} + /> + + + Site URL + + + ( + + )} + /> + + + Summary + + + ( + + )} + /> + + + + {mode.toString() !== FormMode.VIEW && ( + + )} + + +
+
+
+ ) +} + +export default ProjectForm; \ No newline at end of file diff --git a/app2/src/components/projectForm/ProjectFormProps.interface.ts b/app2/src/components/projectForm/ProjectFormProps.interface.ts new file mode 100644 index 0000000..9af6324 --- /dev/null +++ b/app2/src/components/projectForm/ProjectFormProps.interface.ts @@ -0,0 +1,8 @@ +import { FormMode } from "../../enums/formMode"; + +export interface ProjectFormProps { + projectId?: string; + isDrawerOpen: boolean; + mode: FormMode; + onOpenClose: (mode: FormMode) => void; +} \ No newline at end of file diff --git a/app2/src/components/projectForm/ProjectFormState.interface.ts b/app2/src/components/projectForm/ProjectFormState.interface.ts new file mode 100644 index 0000000..78666d8 --- /dev/null +++ b/app2/src/components/projectForm/ProjectFormState.interface.ts @@ -0,0 +1,5 @@ +export interface ProjectFormState { + error: string | undefined; + isDisabled: boolean; + isLoading: boolean; +} \ No newline at end of file diff --git a/app2/src/components/projectForm/reducer.ts b/app2/src/components/projectForm/reducer.ts new file mode 100644 index 0000000..e32c58f --- /dev/null +++ b/app2/src/components/projectForm/reducer.ts @@ -0,0 +1,41 @@ +import { ProjectFormState } from "./ProjectFormState.interface" + +type Action = +| { type: 'SET_ERROR'; payload: string | undefined } +| { type: 'SET_IS_DISABLED'; payload: boolean } +| { type: 'SET_IS_LOADING'; payload: boolean } + +export const initialState: ProjectFormState = { + error: undefined, + isDisabled: false, + isLoading: true, +}; + +export const reducer = ( + state: ProjectFormState, + action: Action +): ProjectFormState => { + switch (action.type) { + case 'SET_ERROR': { + return { + ...state, + error: action.payload + }; + } + case 'SET_IS_DISABLED': { + return { + ...state, + isDisabled: action.payload + }; + } + case 'SET_IS_LOADING': { + return { + ...state, + isLoading: action.payload + }; + } + default: { + return state; + } + } +}; \ No newline at end of file diff --git a/app2/src/components/projects/Project.interface.ts b/app2/src/components/projects/Project.interface.ts new file mode 100644 index 0000000..6d5ab82 --- /dev/null +++ b/app2/src/components/projects/Project.interface.ts @@ -0,0 +1,11 @@ +export interface Project { + partitionKey: string; + rowKey: string; + iconName: string; + order: string; + repoName: string; + siteUrl: string; + subTitle: string; + summary: string; + title: string; +} \ No newline at end of file diff --git a/app2/src/components/projects/Projects.tsx b/app2/src/components/projects/Projects.tsx new file mode 100644 index 0000000..828e148 --- /dev/null +++ b/app2/src/components/projects/Projects.tsx @@ -0,0 +1,248 @@ +import { Project } from './Project.interface'; +import { + Box, + Button, + Card, + CardActions, + CardContent, + CardHeader, + Container, + Grid, + Icon, + IconName, + Typography +} from '@noahspan/noahspan-components'; +import { useIsAuthenticated } from '@azure/msal-react'; +import { FormMode } from '../../enums/formMode'; +import { useEffect, useReducer } from 'react'; +import { initialState, reducer } from './reducer'; +import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; +import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; +import { useAccessToken } from '../../hooks/accessToken/UseAccessToken'; +import ProjectForm from '../projectForm/ProjectForm'; +import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog'; +import ActionMenu from '../actionMenu/ActionMenu'; + +const Projects = () => { + const [state, dispatch] = useReducer(reducer, initialState) + const httpClient: AxiosInstance = useHttpClient(); + const isAuthenticated = useIsAuthenticated(); + const { getAccessToken } = useAccessToken(); + + const findIconByName: IconName = (name: string) => { + const keys = Object.keys(IconName); + + for (const key of keys) { + if (IconName[key] === name) { + return key + } + } + + return undefined; + } + + const getProjects = async (): Promise => { + try { + const config = isAuthenticated + ? { headers: { Authorization: await getAccessToken() } } + : {}; + const response: AxiosResponse = await httpClient.get( + `api/projects`, + config + ); + const projects: Project[] = response.data; + + projects.sort((a, b) => Number(a.order) - Number(b.order)) + + console.log(projects) + + return projects; + } catch (error) { + throw new Error('broken'); + } + }; + + const onOpenCloseProjectForm = (mode: FormMode, projectId?: string) => { + switch (mode) { + case FormMode.ADD: + case FormMode.EDIT: + case FormMode.VIEW: + dispatch({ + type: 'SET_OPEN_CLOSE_PROJECT_FORM', + payload: { + formMode: mode, + selectedProjectId: projectId, + isFormOpen: true + } + }); + + break; + case FormMode.CANCEL: + dispatch({ + type: 'SET_OPEN_CLOSE_PROJECT_FORM', + payload: { + formMode: mode, + selectedProjectId: undefined, + isFormOpen: false + } + }); + + break; + } + }; + + const onDeleteProject = (entryId: string) => { + dispatch({ + type: 'SET_DELETE', + payload: { isConfirmationDialogOpen: true, selectedProjectId: entryId } + }); + }; + + const onConfirmationDialogConfirm = async () => { + try { + dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true }); + + const token = await getAccessToken(); + const config = isAuthenticated + ? { headers: { Authorization: `${token}` } } + : {}; + + // await httpClient.delete(`api/logs/log/${state.selectedEntryId}`, config); + + dispatch({ + type: 'SET_DELETE', + payload: { isConfirmationDialogOpen: false, selectedProjectId: undefined } + }); + // await getProjects(); + } catch (error) { + const axiosError = error as AxiosError; + + dispatch({ + type: 'SET_ERROR', + payload: `Loading of logbook entries failed with the following message: ${axiosError.message}` + }); + } finally { + dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false }); + } + }; + + const onConfirmationDialogCancel = () => { + dispatch({ + type: 'SET_DELETE', + payload: { isConfirmationDialogOpen: false, selectedProjectId: undefined } + }); + }; + + useEffect(() => { + const loadProjects = async () => { + try { + const projects = await getProjects(); + + dispatch({ type: 'SET_PROJECTS', payload: projects }) + } catch (error) { + console.log(error); + } + } + + loadProjects() + }, []); + + return ( + + + + + Projects + + + {isAuthenticated && + + } + + {state.projects.length > 0 && state.projects.map((project: Project) => { + return ( + + + : null} + avatar={} + subheader={project.subTitle} + title={project.title} + slotProps={{ + subheader: { + fontSize: '16px' + }, + title: { + fontSize: '24px', + } + }} + /> + + {project.summary} + + + + + {project.repoName !== '' && + + } + + + {project.siteUrl !== '' && + + } + + + + + + + ) + })} + + {state.isFormOpen && + onOpenCloseProjectForm(mode)} + projectId={state.selectedProjectId} + /> + } + {state.isConfirmDialogOpen && ( + + )} + + + ) +} + +export default Projects; \ No newline at end of file diff --git a/app2/src/components/projects/ProjectsState.interface.ts b/app2/src/components/projects/ProjectsState.interface.ts new file mode 100644 index 0000000..eedefe0 --- /dev/null +++ b/app2/src/components/projects/ProjectsState.interface.ts @@ -0,0 +1,13 @@ +import { FormMode } from '../../enums/formMode'; +import { Project } from './Project.interface'; + +export interface ProjectsState { + projects: Project[]; + formMode: FormMode; + error: string | undefined; + isConfirmDialogLoading: boolean; + isConfirmDialogOpen: boolean; + isFormOpen: boolean; + isLoading: boolean; + selectedProjectId: string | undefined; +} \ No newline at end of file diff --git a/app2/src/components/projects/reducer.ts b/app2/src/components/projects/reducer.ts new file mode 100644 index 0000000..0197218 --- /dev/null +++ b/app2/src/components/projects/reducer.ts @@ -0,0 +1,92 @@ +import { FormMode } from '../../enums/formMode'; +import { ProjectsState } from './ProjectsState.interface' +import { Project } from './Project.interface'; + +type Action = + | { + type: 'SET_DELETE'; + payload: { + isConfirmationDialogOpen: boolean; + selectedProjectId: string | undefined; + }; + } + | { type: 'SET_PROJECTS'; payload: Project[] } + | { type: 'SET_ERROR'; payload: string | undefined } + | { type: 'SET_FORM_MODE'; payload: FormMode } + | { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean } + | { type: 'SET_IS_LOADING'; payload: boolean } + | { + type: 'SET_OPEN_CLOSE_PROJECT_FORM'; + payload: { + formMode: FormMode; + selectedProjectId: string | undefined; + isFormOpen: boolean; + }; + }; + +export const initialState: ProjectsState = { + projects: [], + error: undefined, + formMode: FormMode.CANCEL, + isConfirmDialogLoading: false, + isConfirmDialogOpen: false, + isFormOpen: false, + isLoading: false, + selectedProjectId: undefined +}; + +export const reducer = ( + state: ProjectsState, + action: Action +): ProjectsState => { + switch (action.type) { + case 'SET_DELETE': { + return { + ...state, + isConfirmDialogOpen: action.payload.isConfirmationDialogOpen, + selectedProjectId: action.payload.selectedProjectId + }; + } + case 'SET_PROJECTS': { + return { + ...state, + projects: action.payload + }; + } + case 'SET_ERROR': { + return { + ...state, + error: action.payload + }; + } + case 'SET_FORM_MODE': { + return { + ...state, + formMode: action.payload + }; + } + case 'SET_IS_CONFIRMATION_DIALOG_LOADING': { + return { + ...state, + isConfirmDialogLoading: action.payload + }; + } + case 'SET_IS_LOADING': { + return { + ...state, + isLoading: action.payload + }; + } + case 'SET_OPEN_CLOSE_PROJECT_FORM': { + return { + ...state, + formMode: action.payload.formMode, + isFormOpen: action.payload.isFormOpen, + selectedProjectId: action.payload.selectedProjectId + }; + } + default: { + return state; + } + } +}; diff --git a/app2/src/components/siteNav/SiteNav.tsx b/app2/src/components/siteNav/SiteNav.tsx new file mode 100644 index 0000000..5d6418a --- /dev/null +++ b/app2/src/components/siteNav/SiteNav.tsx @@ -0,0 +1,130 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAppContext } from '../../hooks/appContext/UseAppContext'; +import { + Icon, + MenuItem, + Navbar, + Spinner, + Typography +} from '@noahspan/noahspan-components'; +import { useIsAuthenticated, useMsal } from '@azure/msal-react'; +import { useAccessToken } from '../../hooks/accessToken/UseAccessToken'; +import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; +import { AxiosInstance, AxiosResponse } from 'axios'; +import { User } from '@microsoft/microsoft-graph-types'; + +const SiteNav = () => { + const [loading, setLoading] = useState(false); + const [userPhoto, setUserPhoto] = useState(); + const httpClient: AxiosInstance = useHttpClient(); + const appContext = useAppContext(); + const isAuthenticated = useIsAuthenticated(); + const { getAccessToken } = useAccessToken(); + const { instance } = useMsal(); + const navigate = useNavigate(); + const pages = [ + { + name: 'Projects', + url: '/' + } + ]; + const handleSignIn = () => { + instance.loginRedirect({ + scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`] + }); + }; + const handleSignOut = () => { + instance.logoutRedirect(); + }; + const getUserProfile = async (accessToken: string): Promise => { + try { + const response: AxiosResponse = await httpClient.get(`api/userProfile`, { + headers: { + Authorization: accessToken + } + }); + const userProfile: User = response.data; + + return userProfile; + } catch (error) { + throw new Error(); + } + }; + const getUserPhoto = async (accessToken: string): Promise => { + try { + const response: AxiosResponse = await httpClient.get(`api/userPhoto`, { + headers: { + Authorization: accessToken + }, + responseType: 'arraybuffer' + }); + const arrayBufferView = new Uint8Array(response.data); + const blob = new Blob([arrayBufferView], { type: 'image/png' }); + const imageUrl = window.URL.createObjectURL(blob); + + return imageUrl; + } catch (error) { + console.log(error); + throw new Error(); + } + }; + const handlePageClick = (url: string) => { + navigate(url); + }; + + const Settings = () => { + return ( + + + + Sign Out + + + ); + }; + + useEffect(() => { + const setUserProfile = async () => { + try { + setLoading(true); + + const accessToken: string = await getAccessToken(); + const userProfile = await getUserProfile(accessToken); + const userPhoto = await getUserPhoto(accessToken); + + setUserPhoto(userPhoto); + + appContext.dispatch({ + type: 'SET_USER_PROFILE', + payload: userProfile + }); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + + if ( + isAuthenticated && + Object.keys(appContext.state.userProfile).length === 0 + ) { + setUserProfile(); + } + }, [isAuthenticated]); + + return ( + } + pages={pages} + settings={} + userPhoto={userPhoto} + /> + ); +}; + +export default SiteNav; \ No newline at end of file diff --git a/app/src/context/appContext/AppContext.tsx b/app2/src/context/appContext/AppContext.tsx similarity index 100% rename from app/src/context/appContext/AppContext.tsx rename to app2/src/context/appContext/AppContext.tsx diff --git a/app/src/context/appContext/AppContextProps.interface.ts b/app2/src/context/appContext/AppContextProps.interface.ts similarity index 100% rename from app/src/context/appContext/AppContextProps.interface.ts rename to app2/src/context/appContext/AppContextProps.interface.ts diff --git a/app/src/context/appContext/AppContextProvider.tsx b/app2/src/context/appContext/AppContextProvider.tsx similarity index 100% rename from app/src/context/appContext/AppContextProvider.tsx rename to app2/src/context/appContext/AppContextProvider.tsx diff --git a/app/src/context/appContext/AppContextProviderProps.interface.ts b/app2/src/context/appContext/AppContextProviderProps.interface.ts similarity index 100% rename from app/src/context/appContext/AppContextProviderProps.interface.ts rename to app2/src/context/appContext/AppContextProviderProps.interface.ts diff --git a/app/src/context/appContext/AppContextState.interface.ts b/app2/src/context/appContext/AppContextState.interface.ts similarity index 100% rename from app/src/context/appContext/AppContextState.interface.ts rename to app2/src/context/appContext/AppContextState.interface.ts diff --git a/app/src/context/appContext/reducer.ts b/app2/src/context/appContext/reducer.ts similarity index 100% rename from app/src/context/appContext/reducer.ts rename to app2/src/context/appContext/reducer.ts diff --git a/app2/src/enums/formMode.ts b/app2/src/enums/formMode.ts new file mode 100644 index 0000000..f1eff1f --- /dev/null +++ b/app2/src/enums/formMode.ts @@ -0,0 +1,6 @@ +export enum FormMode { + ADD = 'ADD', + EDIT = 'EDIT', + VIEW = 'VIEW', + CANCEL = 'CANCEL' +} \ No newline at end of file diff --git a/app2/src/hooks/accessToken/UseAccessToken.tsx b/app2/src/hooks/accessToken/UseAccessToken.tsx new file mode 100644 index 0000000..336d9e5 --- /dev/null +++ b/app2/src/hooks/accessToken/UseAccessToken.tsx @@ -0,0 +1,32 @@ +import { + AuthenticationResult, + InteractionRequiredAuthError +} from '@azure/msal-browser'; +import { useMsal } from '@azure/msal-react'; + +export const useAccessToken = () => { + const { accounts, instance } = useMsal(); + const getAccessToken = async () => { + const tokenRequest = { + account: accounts[0], + scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`] + }; + + try { + const response: AuthenticationResult = + await instance.acquireTokenSilent(tokenRequest); + + return `Bearer ${response.accessToken}`; + } catch (error) { + if (error instanceof InteractionRequiredAuthError) { + await instance.acquireTokenRedirect(tokenRequest); + } + + throw error; + } + }; + + return { + getAccessToken + }; +}; diff --git a/app2/src/hooks/appContext/UseAppContext.tsx b/app2/src/hooks/appContext/UseAppContext.tsx new file mode 100644 index 0000000..abb557a --- /dev/null +++ b/app2/src/hooks/appContext/UseAppContext.tsx @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +import { AppContext } from '../../context/appContext/AppContext'; + +export const useAppContext = () => { + const { state, dispatch } = useContext(AppContext); + + return { + state, + dispatch + }; +}; \ No newline at end of file diff --git a/app/src/hooks/httpClient/UseHttpClient.tsx b/app2/src/hooks/httpClient/UseHttpClient.tsx similarity index 100% rename from app/src/hooks/httpClient/UseHttpClient.tsx rename to app2/src/hooks/httpClient/UseHttpClient.tsx diff --git a/app/src/projects/Projects.tsx b/app2/src/index.css similarity index 100% rename from app/src/projects/Projects.tsx rename to app2/src/index.css diff --git a/app2/src/main.tsx b/app2/src/main.tsx new file mode 100644 index 0000000..0dc723a --- /dev/null +++ b/app2/src/main.tsx @@ -0,0 +1,39 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' +import { BrowserRouter } from 'react-router-dom'; +import { AuthenticationResult, EventMessage, EventType, PublicClientApplication } from '@azure/msal-browser'; +import { msalConfig } from './auth/msalConfig.ts'; +import AppContextProvider from './context/appContext/AppContextProvider.tsx'; + +const msalInstance: PublicClientApplication = new PublicClientApplication(msalConfig); + +msalInstance.initialize().then(() => { + const accounts = msalInstance.getAllAccounts(); + + if (accounts.length > 0) { + msalInstance.setActiveAccount(accounts[0]); + } + + msalInstance.addEventCallback((event: EventMessage) => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const payload = event.payload as AuthenticationResult; + const account = payload.account; + + msalInstance.setActiveAccount(account) + } + }) + + createRoot(document.getElementById('root')!).render( + + + + + + + + ) +}) + + diff --git a/app2/src/vite-env.d.ts b/app2/src/vite-env.d.ts new file mode 100644 index 0000000..9bf12a3 --- /dev/null +++ b/app2/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string; + readonly VITE_CLIENT_ID: string; + readonly VITE_TENANT_ID: string; + readonly VITE_REDIRECT_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/app/tsconfig.app.json b/app2/tsconfig.app.json similarity index 100% rename from app/tsconfig.app.json rename to app2/tsconfig.app.json diff --git a/app/tsconfig.json b/app2/tsconfig.json similarity index 100% rename from app/tsconfig.json rename to app2/tsconfig.json diff --git a/app/tsconfig.node.json b/app2/tsconfig.node.json similarity index 100% rename from app/tsconfig.node.json rename to app2/tsconfig.node.json diff --git a/app2/vite.config.ts b/app2/vite.config.ts new file mode 100644 index 0000000..dc1eec9 --- /dev/null +++ b/app2/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + preview: { + port: 8080, + strictPort: true + }, + server: { + port: 8080, + strictPort: true, + host: '0.0.0.0' + } +}); diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7dcd6df --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,11 @@ +version: '3.8' + +services: + azurite: + container_name: azurite + image: mcr.microsoft.com/azure-storage/azurite:3.33.0 + ports: + - '10000:10000' + - '10001:10001' + - '10002:10002' + command: 'azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --loose' \ No newline at end of file diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore new file mode 100644 index 0000000..007a994 --- /dev/null +++ b/infrastructure/.gitignore @@ -0,0 +1,4 @@ +# Terraform +.terraform/* +*.tfstate +*.tfstate.* \ No newline at end of file diff --git a/infrastructure/app_config.tf b/infrastructure/app_config.tf index a0aec55..6740cef 100644 --- a/infrastructure/app_config.tf +++ b/infrastructure/app_config.tf @@ -1,30 +1,30 @@ -data "azurerm_client_config" "current" {} +# data "azurerm_client_config" "current" {} -data "azurerm_user_assigned_identity" "user_assigned_identity" { - name = "noahspanflyingdevuser" - resource_group_name = azurerm_resource_group.resource_group.name -} +# data "azurerm_user_assigned_identity" "user_assigned_identity" { +# name = "noahspanflyingdevuser" +# resource_group_name = azurerm_resource_group.resource_group.name +# } -resource "azurerm_app_configuration" "app_configuration" { - name = "noahspan-appconfig" - resource_group_name = azurerm_resource_group.resource_group.name - location = azurerm_resource_group.resource_group.location +# resource "azurerm_app_configuration" "app_configuration" { +# name = "noahspan-appconfig" +# resource_group_name = azurerm_resource_group.resource_group.name +# location = azurerm_resource_group.resource_group.location - identity { - type = "UserAssigned" - identity_ids = [ - data.azurerm_user_assigned_identity.user_assigned_identity.id - ] - } -} +# identity { +# type = "UserAssigned" +# identity_ids = [ +# data.azurerm_user_assigned_identity.user_assigned_identity.id +# ] +# } +# } -resource "azurerm_role_assignment" "role_assignment" { - scope = azurerm_app_configuration.app_configuration.id - role_definition_name = "App Configuration Data Owner" - principal_id = data.azurerm_client_config.current.object_id -} +# resource "azurerm_role_assignment" "role_assignment" { +# scope = azurerm_app_configuration.app_configuration.id +# role_definition_name = "App Configuration Data Owner" +# principal_id = data.azurerm_client_config.current.object_id +# } # resource "azurerm_app_configuration_feature" "flying-pilots" { # configuration_store_id = azurerm_app_configuration.app_configuration.id diff --git a/infrastructure/config.tf b/infrastructure/config.tf new file mode 100644 index 0000000..ca88f8a --- /dev/null +++ b/infrastructure/config.tf @@ -0,0 +1,4 @@ +module "environment" { + source = "./config" + environment = var.WORKSPACE +} \ No newline at end of file diff --git a/infrastructure/config/main.tf b/infrastructure/config/main.tf new file mode 100644 index 0000000..1a4ffa4 --- /dev/null +++ b/infrastructure/config/main.tf @@ -0,0 +1,52 @@ +locals { + container_app_app_name = { + test = "root-app-test" + prod = "root-app-prod" + } + + container_app_app_container_image = { + test = "noahspan/root-app:v0.0.1" + prod = "noahspan/root-app:v0.0.1" + } + + container_app_app_container_name = { + test = "root-app-test" + prod = "root-app-prod" + } + + container_app_api_name = { + test = "root-api-test" + prod = "root-api-prod" + } + + container_app_api_container_image = { + test = "noahspan/root-api:v0.0.1" + prod = "noahspan/root-api:v0.0.1" + } + + container_app_api_container_name = { + test = "root-api-test" + prod = "root-api-prod" + } + + container_app_api_dapr_app_id = { + test = "rootapitest" + prod = "rootpapiprod" + } + + container_app_environment_name = { + test = "root-test" + prod = "root-prod" + } + + log_analytics_workspace_name = { + test = "root-log-analytics-workspace-test" + prod = "root-log-analytics-workspace-prod" + } + + + storage_account_name = { + test = "noahspanroottest" + prod = "noahspanrootprod" + } +} \ No newline at end of file diff --git a/infrastructure/config/outputs.tf b/infrastructure/config/outputs.tf new file mode 100644 index 0000000..29fa8dd --- /dev/null +++ b/infrastructure/config/outputs.tf @@ -0,0 +1,39 @@ +output "container_app_app_name" { + value = local.container_app_app_name[var.environment] +} + +output "container_app_app_container_image" { + value = local.container_app_app_container_image[var.environment] +} + +output "container_app_app_container_name" { + value = local.container_app_app_container_name[var.environment] +} + +output "container_app_api_name" { + value = local.container_app_api_name[var.environment] +} + +output "container_app_api_container_image" { + value = local.container_app_api_container_image[var.environment] +} + +output "container_app_api_container_name" { + value = local.container_app_api_container_name[var.environment] +} + +output "container_app_api_dapr_app_id" { + value = local.container_app_api_dapr_app_id[var.environment] +} + +output "container_app_environment_name" { + value = local.container_app_environment_name[var.environment] +} + +output "log_analytics_workspace_name" { + value = local.log_analytics_workspace_name[var.environment] +} + +output "storage_account_name" { + value = local.storage_account_name[var.environment] +} \ No newline at end of file diff --git a/infrastructure/config/variables.tf b/infrastructure/config/variables.tf new file mode 100644 index 0000000..9b5503e --- /dev/null +++ b/infrastructure/config/variables.tf @@ -0,0 +1 @@ +variable "environment" {} \ No newline at end of file diff --git a/infrastructure/dns.tf b/infrastructure/dns.tf index a5b40f2..681efb4 100644 --- a/infrastructure/dns.tf +++ b/infrastructure/dns.tf @@ -4,46 +4,46 @@ # validation_type = "dns-txt-token" # } -resource "azurerm_dns_zone" "dns_zone" { - name = var.DOMAIN_NAME - resource_group_name = azurerm_resource_group.resource_group.name -} +# resource "azurerm_dns_zone" "dns_zone" { +# name = var.DOMAIN_NAME +# resource_group_name = azurerm_resource_group.resource_group.name +# } -resource "azurerm_dns_txt_record" "dns_txt_record" { - name = var.WEBSITE_NAME == var.DOMAIN_NAME ? "@" : split(".", var.WEBSITE_NAME)[0] - zone_name = azurerm_dns_zone.dns_zone.name - resource_group_name = azurerm_resource_group.resource_group.name - ttl = 14400 - record { - value = azurerm_static_web_app_custom_domain.static_web_app_custom_domain.validation_token == "" ? "validated" : azurerm_static_web_app_custom_domain.static_web_app_custom_domain.validation_token - } +# resource "azurerm_dns_txt_record" "dns_txt_record" { +# name = var.WEBSITE_NAME == var.DOMAIN_NAME ? "@" : split(".", var.WEBSITE_NAME)[0] +# zone_name = azurerm_dns_zone.dns_zone.name +# resource_group_name = azurerm_resource_group.resource_group.name +# ttl = 14400 +# record { +# value = azurerm_static_web_app_custom_domain.static_web_app_custom_domain.validation_token == "" ? "validated" : azurerm_static_web_app_custom_domain.static_web_app_custom_domain.validation_token +# } - # email - record { - value = var.EMAIL_TXT_VALUE == "" ? "validated" : var.EMAIL_TXT_VALUE - } +# # email +# record { +# value = var.EMAIL_TXT_VALUE == "" ? "validated" : var.EMAIL_TXT_VALUE +# } - record { - value = var.EMAIL_SPF_VALUE == "" ? "validated" : var.EMAIL_SPF_VALUE - } -} +# record { +# value = var.EMAIL_SPF_VALUE == "" ? "validated" : var.EMAIL_SPF_VALUE +# } +# } # email -resource "azurerm_dns_mx_record" "dns_mx_record" { - name = "@" - zone_name = azurerm_dns_zone.dns_zone.name - resource_group_name = azurerm_resource_group.resource_group.name - ttl = 14400 +# resource "azurerm_dns_mx_record" "dns_mx_record" { +# name = "@" +# zone_name = azurerm_dns_zone.dns_zone.name +# resource_group_name = azurerm_resource_group.resource_group.name +# ttl = 14400 - record { - preference = 10 - exchange = "mx01.mail.icloud.com." - } +# record { +# preference = 10 +# exchange = "mx01.mail.icloud.com." +# } - record { - preference = 10 - exchange = "mx02.mail.icloud.com." - } -} +# record { +# preference = 10 +# exchange = "mx02.mail.icloud.com." +# } +# } diff --git a/infrastructure/main.tf b/infrastructure/main.tf index 764520b..e03e45e 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -8,38 +8,198 @@ # subdomain_name = var.SUBDOMAIN_NAME # } -resource "azurerm_static_web_app" "static_web_app" { - name = var.STATIC_WEB_APP_NAME - resource_group_name = azurerm_resource_group.resource_group.name - location = var.REGION -} +# resource "azurerm_static_web_app" "static_web_app" { +# name = var.STATIC_WEB_APP_NAME +# resource_group_name = azurerm_resource_group.resource_group.name +# location = var.REGION +# } -resource "azurerm_static_web_app_custom_domain" "static_web_app_custom_domain" { - static_web_app_id = azurerm_static_web_app.static_web_app.id - domain_name = var.WEBSITE_NAME - validation_type = "dns-txt-token" -} +# resource "azurerm_static_web_app_custom_domain" "static_web_app_custom_domain" { +# static_web_app_id = azurerm_static_web_app.static_web_app.id +# domain_name = var.WEBSITE_NAME +# validation_type = "dns-txt-token" +# } -resource "azurerm_dns_a_record" "dns_a_record" { - name = var.WEBSITE_NAME == var.DOMAIN_NAME ? "@" : split(".", var.WEBSITE_NAME)[0] - zone_name = azurerm_dns_zone.dns_zone.name - resource_group_name = azurerm_resource_group.resource_group.name - ttl = 14400 - target_resource_id = azurerm_static_web_app.static_web_app.id -} +# resource "azurerm_dns_a_record" "dns_a_record" { +# name = var.WEBSITE_NAME == var.DOMAIN_NAME ? "@" : split(".", var.WEBSITE_NAME)[0] +# zone_name = azurerm_dns_zone.dns_zone.name +# resource_group_name = azurerm_resource_group.resource_group.name +# ttl = 14400 +# target_resource_id = azurerm_static_web_app.static_web_app.id +# } -resource "azurerm_dns_cname_record" "dns_cname_record" { - name = "www" - zone_name = azurerm_dns_zone.dns_zone.name - resource_group_name = azurerm_resource_group.resource_group.name - ttl = 14400 - record = azurerm_static_web_app.static_web_app.default_host_name -} +# resource "azurerm_dns_cname_record" "dns_cname_record" { +# name = "www" +# zone_name = azurerm_dns_zone.dns_zone.name +# resource_group_name = azurerm_resource_group.resource_group.name +# ttl = 14400 +# record = azurerm_static_web_app.static_web_app.default_host_name +# } -resource "azurerm_static_web_app_custom_domain" "static_web_app_custom_domain_www" { - static_web_app_id = azurerm_static_web_app.static_web_app.id - domain_name = "www.${var.DOMAIN_NAME}" - validation_type = "cname-delegation" +# resource "azurerm_static_web_app_custom_domain" "static_web_app_custom_domain_www" { +# static_web_app_id = azurerm_static_web_app.static_web_app.id +# domain_name = "www.${var.DOMAIN_NAME}" +# validation_type = "cname-delegation" - depends_on = [ azurerm_dns_cname_record.dns_cname_record ] -} \ No newline at end of file +# depends_on = [ azurerm_dns_cname_record.dns_cname_record ] +# } + + +# data "azurerm_client_config" "current" { +# } + +# resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { +# name = module.environment.log_analytics_workspace_name +# location = data.azurerm_resource_group.resource_group.location +# resource_group_name = data.azurerm_resource_group.resource_group.name +# sku = "PerGB2018" +# retention_in_days = 30 +# } + +# resource "azurerm_container_app_environment" "container_app_environment" { +# name = module.environment.container_app_environment_name +# location = data.azurerm_resource_group.resource_group.location +# resource_group_name = data.azurerm_resource_group.resource_group.name +# log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.id +# } + +# resource "azurerm_container_app" "container_app_api" { +# name = module.environment.container_app_api_name +# container_app_environment_id = azurerm_container_app_environment.container_app_environment.id +# resource_group_name = data.azurerm_resource_group.resource_group.name +# revision_mode = "Single" + +# registry { +# server = "index.docker.io" +# username = var.DOCKER_IO_USERNAME +# password_secret_name = "docker-io-password" +# } + +# template { +# min_replicas = 0 +# max_replicas = 2 + +# container { +# name = module.environment.container_app_api_container_name +# image = module.environment.container_app_api_container_image +# cpu = 0.25 +# memory = "0.5Gi" + +# env { +# name = "AZURE_STORAGE_CONNECTION_STRING" +# secret_name = "azure-storage-connection-string" +# } + +# env { +# name = "CLIENT_ID" +# secret_name = "client-id" +# } + +# env { +# name = "CLIENT_SECRET" +# secret_name = "client-secret" +# } + +# env { +# name = "TENANT_ID" +# secret_name = "tenant-id" +# } +# } +# } + +# ingress { +# allow_insecure_connections = false +# external_enabled = true +# target_port = 3000 +# transport = "auto" + +# traffic_weight { +# latest_revision = true +# percentage = 100 +# } +# } + +# secret { +# name = "docker-io-password" +# value = var.DOCKER_IO_PASSWORD +# } + +# secret { +# name = "azure-storage-connection-string" +# value = azurerm_storage_account.storage_account.primary_connection_string +# } + +# secret { +# name = "client-id" +# value = var.CLIENT_ID +# } + +# secret { +# name = "client-secret" +# value = var.CLIENT_SECRET +# } + +# secret { +# name = "tenant-id" +# value = var.TENANT_ID +# } + +# lifecycle { +# ignore_changes = [ template[0].container[0].image ] +# } +# } + +# resource "azurerm_container_app" "container_app_app" { +# name = module.environment.container_app_app_name +# container_app_environment_id = azurerm_container_app_environment.container_app_environment.id +# resource_group_name = data.azurerm_resource_group.resource_group.name +# revision_mode = "Single" + +# registry { +# server = "index.docker.io" +# username = var.DOCKER_IO_USERNAME +# password_secret_name = "docker-io-password" +# } + +# template { +# min_replicas = 0 +# max_replicas = 2 + +# container { +# name = module.environment.container_app_app_container_name +# image = module.environment.container_app_app_container_image +# cpu = 0.25 +# memory = "0.5Gi" +# } +# } + +# ingress { +# allow_insecure_connections = false +# external_enabled = true +# target_port = 8080 +# transport = "auto" + +# traffic_weight { +# latest_revision = true +# percentage = 100 +# } +# } + +# secret { +# name = "docker-io-password" +# value = var.DOCKER_IO_PASSWORD +# } + +# lifecycle { +# ignore_changes = [ template[0].container[0].image ] +# } +# } + +# resource "azurerm_container_app_custom_domain" "custom_domain" { +# name = trimsuffix(trimprefix(azurerm_dns_txt_record.dns_txt_record.fqdn, "asuid."), ".") +# container_app_id = azurerm_container_app.container_app_app.id + +# lifecycle { +# ignore_changes = [ certificate_binding_type, container_app_environment_certificate_id ] +# } +# } \ No newline at end of file diff --git a/infrastructure/modules/static_web_app/outputs.tf b/infrastructure/modules/static_web_app/outputs.tf index 3fec2d3..0754524 100644 --- a/infrastructure/modules/static_web_app/outputs.tf +++ b/infrastructure/modules/static_web_app/outputs.tf @@ -1,11 +1,11 @@ -output "id" { - value = azurerm_static_web_app.static_web_app.id -} +# output "id" { +# value = azurerm_static_web_app.static_web_app.id +# } -output "api_key" { - value = azurerm_static_web_app.static_web_app.api_key -} +# output "api_key" { +# value = azurerm_static_web_app.static_web_app.api_key +# } -output "default_host_name" { - value = azurerm_static_web_app.static_web_app.default_host_name -} \ No newline at end of file +# output "default_host_name" { +# value = azurerm_static_web_app.static_web_app.default_host_name +# } \ No newline at end of file diff --git a/infrastructure/modules/static_web_app/variables.tf b/infrastructure/modules/static_web_app/variables.tf index dd13bbc..a3dc9c9 100644 --- a/infrastructure/modules/static_web_app/variables.tf +++ b/infrastructure/modules/static_web_app/variables.tf @@ -1,24 +1,24 @@ -variable "region" { +# variable "region" { -} +# } -variable "resource_group_name" { +# variable "resource_group_name" { -} +# } -variable "static_web_app_name" { +# variable "static_web_app_name" { -} +# } -variable "custom_domain_name_count" { +# variable "custom_domain_name_count" { -} +# } -variable "domain_name" { +# variable "domain_name" { -} +# } -variable "subdomain_name" { +# variable "subdomain_name" { -} +# } diff --git a/infrastructure/outputs.tf b/infrastructure/outputs.tf index 28251c7..a26f319 100644 --- a/infrastructure/outputs.tf +++ b/infrastructure/outputs.tf @@ -1,7 +1,7 @@ -output "api_key" { - value = azurerm_static_web_app.static_web_app.api_key - sensitive = true -} +# output "api_key" { +# value = azurerm_static_web_app.static_web_app.api_key +# sensitive = true +# } # output "default_host_name" { # value = azurerm_static_web_app.static_web_app.default_host_name @@ -12,6 +12,6 @@ output "api_key" { # sensitive = false # } -output "name_servers" { - value = azurerm_dns_zone.dns_zone.name_servers -} \ No newline at end of file +# output "name_servers" { +# value = azurerm_dns_zone.dns_zone.name_servers +# } \ No newline at end of file diff --git a/infrastructure/resource_group.tf b/infrastructure/resource_group.tf index a8abc53..cbaf81a 100644 --- a/infrastructure/resource_group.tf +++ b/infrastructure/resource_group.tf @@ -1,4 +1,3 @@ -resource "azurerm_resource_group" "resource_group" { +data "azurerm_resource_group" "resource_group" { name = var.RESOURCE_GROUP_NAME - location = var.REGION } \ No newline at end of file diff --git a/infrastructure/storage_account.tf b/infrastructure/storage_account.tf new file mode 100644 index 0000000..b188d1d --- /dev/null +++ b/infrastructure/storage_account.tf @@ -0,0 +1,12 @@ +resource "azurerm_storage_account" "storage_account" { + name = module.environment.storage_account_name + resource_group_name = data.azurerm_resource_group.resource_group.name + location = data.azurerm_resource_group.resource_group.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_table" "projects_table" { + name = "projects" + storage_account_name = azurerm_storage_account.storage_account.name +} \ No newline at end of file diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf index 393e5bc..76a21dd 100644 --- a/infrastructure/variables.tf +++ b/infrastructure/variables.tf @@ -1,31 +1,46 @@ -variable "EMAIL_TXT_VALUE" { +variable "API_SUBDOMAIN_NAME" { type = string } -variable "EMAIL_SPF_VALUE" { +variable "APP_SUBDOMAIN_NAME" { type = string } +variable "CLIENT_ID" { + type = string +} + +variable "CLIENT_SECRET" { + type = string + sensitive = true +} + +variable "DOCKER_IO_PASSWORD" { + type = string + sensitive = true +} + +variable "DOCKER_IO_USERNAME" { + type = string +} + +variable "DOMAIN_NAME" { + type = string +} + variable "REGION" { - type = string + type = string } variable "RESOURCE_GROUP_NAME" { type = string } -variable "STATIC_WEB_APP_NAME" { +variable "TENANT_ID" { type = string } -variable "CUSTOM_DOMAIN_NAME_COUNT" { - type = number -} - -variable "DOMAIN_NAME" { +variable "WORKSPACE" { type = string } -variable "WEBSITE_NAME" { - type = string -} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1660a95..aa41f01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,7 +120,7 @@ importers: specifier: ^5.1.3 version: 5.7.3 - app: + app2: dependencies: '@azure/msal-browser': specifier: ^4.2.1 @@ -128,9 +128,12 @@ importers: '@azure/msal-react': specifier: ^3.0.4 version: 3.0.4(@azure/msal-browser@4.2.1)(react@19.0.0) + '@microsoft/microsoft-graph-types': + specifier: ^2.40.0 + version: 2.40.0 '@noahspan/noahspan-components': - specifier: ^1.0.0 - version: 1.0.0(@mui/system@6.4.3(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(less@4.2.2)(moment@2.30.1)(postcss@8.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.4(prettier@3.5.0))(typescript@5.7.3)(webpack@5.97.1(esbuild@0.18.20)) + specifier: ^1.3.1 + version: 1.3.1(@mui/system@6.4.3(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(less@4.2.2)(moment@2.30.1)(postcss@8.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.4(prettier@3.5.0))(typescript@5.7.3)(webpack@5.97.1(esbuild@0.18.20)) axios: specifier: ^1.7.9 version: 1.7.9 @@ -140,6 +143,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-hook-form: + specifier: ^7.54.2 + version: 7.54.2(react@19.0.0) react-router-dom: specifier: ^7.1.5 version: 7.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -147,9 +153,6 @@ importers: '@eslint/js': specifier: ^9.19.0 version: 9.20.0 - '@microsoft/microsoft-graph-types': - specifier: ^2.40.0 - version: 2.40.0 '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -170,7 +173,7 @@ importers: version: 0.4.19(eslint@9.20.1(jiti@1.21.7)) globals: specifier: ^15.14.0 - version: 15.14.0 + version: 15.15.0 typescript: specifier: ~5.7.2 version: 5.7.3 @@ -1428,8 +1431,8 @@ packages: '@nestjs/common': ^9.0.0 || ^10.0.0 '@nestjs/core': ^9.0.0 || ^10.0.0 - '@noahspan/noahspan-components@1.0.0': - resolution: {integrity: sha512-MZaWmqN+Ci41mzVyejhlKE/1BbnYZRS/tekNgO5ZNPgzfS09TIdUb3at+KQhwqw2EQ4b4osk3RVzhIGxuSHYnA==} + '@noahspan/noahspan-components@1.3.1': + resolution: {integrity: sha512-7eyaxfR5NZIuFaAInT0v/n2O8ULE1zalMwaHvXN2SiU540FTNOGoH7fjd6DsY6RY+FBVJBD0Wo0gfzTy7RbPFg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.2.0 @@ -1905,98 +1908,98 @@ packages: '@radix-ui/rect@1.0.1': resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} - '@rollup/rollup-android-arm-eabi@4.34.6': - resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} + '@rollup/rollup-android-arm-eabi@4.34.7': + resolution: {integrity: sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.6': - resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} + '@rollup/rollup-android-arm64@4.34.7': + resolution: {integrity: sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.6': - resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} + '@rollup/rollup-darwin-arm64@4.34.7': + resolution: {integrity: sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.6': - resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} + '@rollup/rollup-darwin-x64@4.34.7': + resolution: {integrity: sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.6': - resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} + '@rollup/rollup-freebsd-arm64@4.34.7': + resolution: {integrity: sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.6': - resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} + '@rollup/rollup-freebsd-x64@4.34.7': + resolution: {integrity: sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.6': - resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} + '@rollup/rollup-linux-arm-gnueabihf@4.34.7': + resolution: {integrity: sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.34.6': - resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} + '@rollup/rollup-linux-arm-musleabihf@4.34.7': + resolution: {integrity: sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.6': - resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} + '@rollup/rollup-linux-arm64-gnu@4.34.7': + resolution: {integrity: sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.34.6': - resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} + '@rollup/rollup-linux-arm64-musl@4.34.7': + resolution: {integrity: sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.34.6': - resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} + '@rollup/rollup-linux-loongarch64-gnu@4.34.7': + resolution: {integrity: sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': - resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.34.7': + resolution: {integrity: sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.34.6': - resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} + '@rollup/rollup-linux-riscv64-gnu@4.34.7': + resolution: {integrity: sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.34.6': - resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} + '@rollup/rollup-linux-s390x-gnu@4.34.7': + resolution: {integrity: sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.6': - resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} + '@rollup/rollup-linux-x64-gnu@4.34.7': + resolution: {integrity: sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.34.6': - resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} + '@rollup/rollup-linux-x64-musl@4.34.7': + resolution: {integrity: sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.34.6': - resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} + '@rollup/rollup-win32-arm64-msvc@4.34.7': + resolution: {integrity: sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.6': - resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} + '@rollup/rollup-win32-ia32-msvc@4.34.7': + resolution: {integrity: sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.6': - resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} + '@rollup/rollup-win32-x64-msvc@4.34.7': + resolution: {integrity: sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==} cpu: [x64] os: [win32] @@ -3525,8 +3528,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.14.0: - resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} globby@11.1.0: @@ -4642,6 +4645,12 @@ packages: peerDependencies: react: ^19.0.0 + react-hook-form@7.54.2: + resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4805,8 +4814,8 @@ packages: resolution: {integrity: sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==} engines: {node: '>=18.0'} - rollup@4.34.6: - resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} + rollup@4.34.7: + resolution: {integrity: sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -6992,7 +7001,7 @@ snapshots: - stream-browserify - supports-color - '@noahspan/noahspan-components@1.0.0(@mui/system@6.4.3(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(less@4.2.2)(moment@2.30.1)(postcss@8.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.4(prettier@3.5.0))(typescript@5.7.3)(webpack@5.97.1(esbuild@0.18.20))': + '@noahspan/noahspan-components@1.3.1(@mui/system@6.4.3(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0))(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(less@4.2.2)(moment@2.30.1)(postcss@8.5.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.4(prettier@3.5.0))(typescript@5.7.3)(webpack@5.97.1(esbuild@0.18.20))': dependencies: '@emotion/react': 11.14.0(@types/react@19.0.8)(react@19.0.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.8)(react@19.0.0))(@types/react@19.0.8)(react@19.0.0) @@ -7502,61 +7511,61 @@ snapshots: dependencies: '@babel/runtime': 7.26.7 - '@rollup/rollup-android-arm-eabi@4.34.6': + '@rollup/rollup-android-arm-eabi@4.34.7': optional: true - '@rollup/rollup-android-arm64@4.34.6': + '@rollup/rollup-android-arm64@4.34.7': optional: true - '@rollup/rollup-darwin-arm64@4.34.6': + '@rollup/rollup-darwin-arm64@4.34.7': optional: true - '@rollup/rollup-darwin-x64@4.34.6': + '@rollup/rollup-darwin-x64@4.34.7': optional: true - '@rollup/rollup-freebsd-arm64@4.34.6': + '@rollup/rollup-freebsd-arm64@4.34.7': optional: true - '@rollup/rollup-freebsd-x64@4.34.6': + '@rollup/rollup-freebsd-x64@4.34.7': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + '@rollup/rollup-linux-arm-gnueabihf@4.34.7': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.6': + '@rollup/rollup-linux-arm-musleabihf@4.34.7': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.6': + '@rollup/rollup-linux-arm64-gnu@4.34.7': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.6': + '@rollup/rollup-linux-arm64-musl@4.34.7': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + '@rollup/rollup-linux-loongarch64-gnu@4.34.7': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + '@rollup/rollup-linux-powerpc64le-gnu@4.34.7': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.6': + '@rollup/rollup-linux-riscv64-gnu@4.34.7': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.6': + '@rollup/rollup-linux-s390x-gnu@4.34.7': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.6': + '@rollup/rollup-linux-x64-gnu@4.34.7': optional: true - '@rollup/rollup-linux-x64-musl@4.34.6': + '@rollup/rollup-linux-x64-musl@4.34.7': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.6': + '@rollup/rollup-win32-arm64-msvc@4.34.7': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.6': + '@rollup/rollup-win32-ia32-msvc@4.34.7': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.6': + '@rollup/rollup-win32-x64-msvc@4.34.7': optional: true '@schematics/angular@19.1.6(chokidar@3.6.0)': @@ -9517,7 +9526,7 @@ snapshots: globals@14.0.0: {} - globals@15.14.0: {} + globals@15.15.0: {} globby@11.1.0: dependencies: @@ -10823,6 +10832,10 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-hook-form@7.54.2(react@19.0.0): + dependencies: + react: 19.0.0 + react-is@16.13.1: {} react-is@18.3.1: {} @@ -10977,29 +10990,29 @@ snapshots: safe-stable-stringify: 2.5.0 semver-compare: 1.0.0 - rollup@4.34.6: + rollup@4.34.7: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.6 - '@rollup/rollup-android-arm64': 4.34.6 - '@rollup/rollup-darwin-arm64': 4.34.6 - '@rollup/rollup-darwin-x64': 4.34.6 - '@rollup/rollup-freebsd-arm64': 4.34.6 - '@rollup/rollup-freebsd-x64': 4.34.6 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 - '@rollup/rollup-linux-arm-musleabihf': 4.34.6 - '@rollup/rollup-linux-arm64-gnu': 4.34.6 - '@rollup/rollup-linux-arm64-musl': 4.34.6 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 - '@rollup/rollup-linux-riscv64-gnu': 4.34.6 - '@rollup/rollup-linux-s390x-gnu': 4.34.6 - '@rollup/rollup-linux-x64-gnu': 4.34.6 - '@rollup/rollup-linux-x64-musl': 4.34.6 - '@rollup/rollup-win32-arm64-msvc': 4.34.6 - '@rollup/rollup-win32-ia32-msvc': 4.34.6 - '@rollup/rollup-win32-x64-msvc': 4.34.6 + '@rollup/rollup-android-arm-eabi': 4.34.7 + '@rollup/rollup-android-arm64': 4.34.7 + '@rollup/rollup-darwin-arm64': 4.34.7 + '@rollup/rollup-darwin-x64': 4.34.7 + '@rollup/rollup-freebsd-arm64': 4.34.7 + '@rollup/rollup-freebsd-x64': 4.34.7 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.7 + '@rollup/rollup-linux-arm-musleabihf': 4.34.7 + '@rollup/rollup-linux-arm64-gnu': 4.34.7 + '@rollup/rollup-linux-arm64-musl': 4.34.7 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.7 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.7 + '@rollup/rollup-linux-riscv64-gnu': 4.34.7 + '@rollup/rollup-linux-s390x-gnu': 4.34.7 + '@rollup/rollup-linux-x64-gnu': 4.34.7 + '@rollup/rollup-linux-x64-musl': 4.34.7 + '@rollup/rollup-win32-arm64-msvc': 4.34.7 + '@rollup/rollup-win32-ia32-msvc': 4.34.7 + '@rollup/rollup-win32-x64-msvc': 4.34.7 fsevents: 2.3.3 run-async@2.4.1: {} @@ -11548,7 +11561,7 @@ snapshots: dependencies: esbuild: 0.24.2 postcss: 8.5.2 - rollup: 4.34.6 + rollup: 4.34.7 optionalDependencies: '@types/node': 20.17.17 fsevents: 2.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7352cff..b075621 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'api' - - 'app' \ No newline at end of file + - 'app' + - 'app2' \ No newline at end of file