first commit
8
.env.sample
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
NEXT_PUBLIC_CERBEROS_API_URL = // URL for the Cerberos BFF API
|
||||||
|
NEXT_PUBLIC_API_URL = // Base URL for blueprint BFF layer(this was included before cerberos implementation)
|
||||||
|
NEXT_PUBLIC_APP_ID = // Application ID for client identification(cliendId)
|
||||||
|
NEXT_PUBLIC_REDIRECT_URI = // URI to redirect after authentication(blueprint uri + /main)
|
||||||
|
NEXT_PUBLIC_SCOPE = // Permissions scope for the application(should be User.Read)
|
||||||
|
NEXT_PUBLIC_AUTHORITY = // Authority URL for authentication, e.g., https://{domain}.ciamlogin.com/{tenandId}/
|
||||||
|
NEXT_PUBLIC_LOGOUT_URI = // URI to redirect after logout(blueprint uri + /logout)
|
||||||
|
NEXT_PUBLIC_ACCESS_AS_USER = // access as user scope api://{clientId}/{access as user scope}
|
||||||
51
.gitignore
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
### NextJS ###
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
.env*.development
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
*.log
|
||||||
|
logs
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
59
.pipelines/GitVersion.yml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
mode: mainline
|
||||||
|
assembly-versioning-scheme: MajorMinorPatch
|
||||||
|
tag-prefix: '[vV]'
|
||||||
|
major-version-bump-message: '\+semver:\s?(breaking|major)'
|
||||||
|
minor-version-bump-message: '\+semver:\s?(feature|minor)'
|
||||||
|
patch-version-bump-message: '\+semver:\s?(fix|patch)'
|
||||||
|
no-bump-message: '\+semver:\s?(none|skip)'
|
||||||
|
legacy-semver-padding: 4
|
||||||
|
build-metadata-padding: 4
|
||||||
|
commits-since-version-source-padding: 4
|
||||||
|
commit-message-incrementing: Enabled
|
||||||
|
branches:
|
||||||
|
main:
|
||||||
|
regex: ^master$|^main$
|
||||||
|
tag: ''
|
||||||
|
increment: Patch
|
||||||
|
prevent-increment-of-merged-branch-version: true
|
||||||
|
track-merge-target: false
|
||||||
|
source-branches: [ 'develop', 'release' ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: false
|
||||||
|
is-mainline: true
|
||||||
|
pre-release-weight: 55000
|
||||||
|
feature:
|
||||||
|
regex: ^features?[/-]
|
||||||
|
tag: useBranchName
|
||||||
|
increment: Inherit
|
||||||
|
prevent-increment-of-merged-branch-version: false
|
||||||
|
track-merge-target: false
|
||||||
|
source-branches: [ 'develop', 'main', 'release', 'feature', 'support', 'hotfix' ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: false
|
||||||
|
is-mainline: false
|
||||||
|
pre-release-weight: 30000
|
||||||
|
release:
|
||||||
|
regex: ^releases?[/-]
|
||||||
|
tag: beta
|
||||||
|
increment: None
|
||||||
|
is-mainline: true
|
||||||
|
regex: ^releases?[/-]
|
||||||
|
prevent-increment-of-merged-branch-version: true
|
||||||
|
track-merge-target: false
|
||||||
|
source-branches: [ 'develop', 'main', 'support', 'release' ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: true
|
||||||
|
is-mainline: false
|
||||||
|
pre-release-weight: 30000
|
||||||
|
hotfix:
|
||||||
|
regex: ^hotfix(es)?[/-]
|
||||||
|
increment: Patch
|
||||||
|
prevent-increment-of-merged-branch-version: false
|
||||||
|
track-merge-target: false
|
||||||
|
source-branches: [ 'develop', 'main', 'support' ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: false
|
||||||
|
is-mainline: false
|
||||||
|
pre-release-weight: 30000
|
||||||
|
ignore:
|
||||||
|
sha: []
|
||||||
107
.pipelines/base-pipeline.yml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# Starter pipeline
|
||||||
|
# Start with a minimal pipeline that you can customize to build and deploy your code.
|
||||||
|
# Add steps that build, run tests, deploy, and more:
|
||||||
|
# https://aka.ms/yaml
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
- development
|
||||||
|
- releases/*
|
||||||
|
- feature/*
|
||||||
|
- hotfix/*
|
||||||
|
- bugfix/*
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- group: blueprint-sandbox-web
|
||||||
|
- name: isReleaseDeployment
|
||||||
|
value: $[eq(variables['Build.SourceBranch'], 'refs/heads/development')]
|
||||||
|
- name: imageName
|
||||||
|
value: $[variables.containerImageName]
|
||||||
|
- name: containerRegistryEndpoint
|
||||||
|
value: $[variables.containerRegistryEndpointUrl]
|
||||||
|
- name: webAppName
|
||||||
|
value: $[variables.appServiceName]
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout: self
|
||||||
|
fetchDepth: 0 # It will fix gitversion iteraction to extract the correct version from our history
|
||||||
|
persistCredentials: true # It will fix terminal user to be able to push tag version on the build.
|
||||||
|
displayName: '[Step1.0] Define fetchDepth'
|
||||||
|
|
||||||
|
|
||||||
|
# Install gitversion tool
|
||||||
|
# - task: gitversion/setup@3
|
||||||
|
# displayName: '[Step2.1] Install Git Tools'
|
||||||
|
# inputs:
|
||||||
|
# versionSpec: '5.x'
|
||||||
|
|
||||||
|
# Execute the tool to identify the next SemVersion for this library
|
||||||
|
# - task: gitversion/execute@3
|
||||||
|
# displayName: '[Step2.2] Calculate SemVer'
|
||||||
|
# inputs:
|
||||||
|
# useConfigFile: true
|
||||||
|
# configFilePath: '.pipelines/GitVersion.yml'
|
||||||
|
|
||||||
|
# Echo the SemVersion Identified
|
||||||
|
- script: echo current version is $(GitVersion.SemVer) $(Build.SourcesDirectory)
|
||||||
|
displayName: '[Step2.3] Display calculated version'
|
||||||
|
|
||||||
|
- script: npm ci
|
||||||
|
displayName: '[Step3.1] NPM CI'
|
||||||
|
|
||||||
|
- task: SnykSecurityScan@1
|
||||||
|
displayName: '[Step3.2] Snyk Scanning'
|
||||||
|
inputs:
|
||||||
|
serviceConnectionEndpoint: 'SnykConnection'
|
||||||
|
testType: 'app'
|
||||||
|
severityThreshold: 'high'
|
||||||
|
monitorWhen: 'noIssuesFound'
|
||||||
|
failOnIssues: true
|
||||||
|
additionalArguments: '--file=package.json'
|
||||||
|
|
||||||
|
- task: Docker@2
|
||||||
|
displayName: '[Step4.1] Build'
|
||||||
|
inputs:
|
||||||
|
containerRegistry: '$(containerRegistryServiceConnectionName)'
|
||||||
|
repository: '$(imageName)'
|
||||||
|
command: 'build'
|
||||||
|
# tags: '$(GitVersion.SemVer)'
|
||||||
|
tags: 'sandbox'
|
||||||
|
Dockerfile: '**/Dockerfile'
|
||||||
|
arguments: --build-arg NEXT_PUBLIC_API_URL=$(NEXT_PUBLIC_API_URL) --build-arg NEXT_PUBLIC_CERBEROS_API_URL=$(NEXT_PUBLIC_CERBEROS_API_URL) --build-arg NEXT_PUBLIC_APP_ID=$(NEXT_PUBLIC_APP_ID) --build-arg NEXT_PUBLIC_REDIRECT_URI=$(NEXT_PUBLIC_REDIRECT_URI) --build-arg NEXT_PUBLIC_SCOPE=$(NEXT_PUBLIC_SCOPE) --build-arg NEXT_PUBLIC_AUTHORITY=$(NEXT_PUBLIC_AUTHORITY) --build-arg NEXT_PUBLIC_LOGOUT_URI=$(NEXT_PUBLIC_LOGOUT_URI) --build-arg NEXT_PUBLIC_ACCESS_AS_USER=$(NEXT_PUBLIC_ACCESS_AS_USER)
|
||||||
|
|
||||||
|
# - task: SnykSecurityScan@1
|
||||||
|
# inputs:
|
||||||
|
# serviceConnectionEndpoint: 'SnykConnection'
|
||||||
|
# testType: 'container'
|
||||||
|
# # dockerImageName: '$(containerRegistryEndpoint)/$(imageName):$(GitVersion.SemVer)'
|
||||||
|
# dockerImageName: '$(containerRegistryEndpoint)/$(imageName):sandbox'
|
||||||
|
# severityThreshold: 'high'
|
||||||
|
# monitorWhen: 'noIssuesFound'
|
||||||
|
# failOnIssues: true
|
||||||
|
|
||||||
|
- task: Docker@2
|
||||||
|
displayName: '[Step4.1] Push'
|
||||||
|
inputs:
|
||||||
|
containerRegistry: '$(containerRegistryServiceConnectionName)'
|
||||||
|
repository: '$(imageName)'
|
||||||
|
command: 'push'
|
||||||
|
# tags: '$(GitVersion.SemVer)'
|
||||||
|
tags: 'sandbox'
|
||||||
|
Dockerfile: '**/Dockerfile'
|
||||||
|
|
||||||
|
- task: AzureRmWebAppDeployment@4
|
||||||
|
displayName: 'Deploy on Sandbox'
|
||||||
|
condition: and(succeeded(), eq(variables.isReleaseDeployment, true))
|
||||||
|
enabled: true
|
||||||
|
inputs:
|
||||||
|
ConnectionType: 'AzureRM'
|
||||||
|
azureSubscription: '$(azureRMServiceConnectionName)'
|
||||||
|
appType: 'webAppContainer'
|
||||||
|
WebAppName: '$(webAppName)'
|
||||||
|
DockerNamespace: '$(containerRegistryEndpoint)'
|
||||||
|
DockerRepository: '$(imageName)'
|
||||||
|
# DockerImageTag: '$(GitVersion.SemVer)'
|
||||||
|
DockerImageTag: 'sandbox'
|
||||||
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
113
Dockerfile
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
FROM node:lts-alpine3.18 AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies based on the preferred package manager
|
||||||
|
# It should use .npmrc
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN \
|
||||||
|
if [ -f package-lock.json ]; then npm ci; \
|
||||||
|
else echo "Lockfile not found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
|
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_API_URL
|
||||||
|
ARG NEXT_PUBLIC_CERBEROS_API_URL
|
||||||
|
ARG NEXT_PUBLIC_APP_ID
|
||||||
|
ARG NEXT_PUBLIC_REDIRECT_URI
|
||||||
|
ARG NEXT_PUBLIC_SCOPE
|
||||||
|
ARG NEXT_PUBLIC_AUTHORITY
|
||||||
|
ARG NEXT_PUBLIC_LOGOUT_URI
|
||||||
|
ARG NEXT_PUBLIC_ACCESS_AS_USER
|
||||||
|
|
||||||
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||||
|
ENV NEXT_PUBLIC_CERBEROS_API_URL=$NEXT_PUBLIC_CERBEROS_API_URL
|
||||||
|
ENV NEXT_PUBLIC_APP_ID=$NEXT_PUBLIC_APP_ID
|
||||||
|
ENV NEXT_PUBLIC_REDIRECT_URI=$NEXT_PUBLIC_REDIRECT_URI
|
||||||
|
ENV NEXT_PUBLIC_SCOPE=$NEXT_PUBLIC_SCOPE
|
||||||
|
ENV NEXT_PUBLIC_AUTHORITY=$NEXT_PUBLIC_AUTHORITY
|
||||||
|
ENV NEXT_PUBLIC_LOGOUT_URI=$NEXT_PUBLIC_LOGOUT_URI
|
||||||
|
ENV NEXT_PUBLIC_ACCESS_AS_USER=$NEXT_PUBLIC_ACCESS_AS_USER
|
||||||
|
|
||||||
|
RUN echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL"
|
||||||
|
RUN echo "NEXT_PUBLIC_CERBEROS_API_URL=$NEXT_PUBLIC_CERBEROS_API_URL"
|
||||||
|
RUN echo "NEXT_PUBLIC_APP_ID=$NEXT_PUBLIC_APP_ID"
|
||||||
|
RUN echo "NEXT_PUBLIC_REDIRECT_URI=$NEXT_PUBLIC_REDIRECT_URI"
|
||||||
|
RUN echo "NEXT_PUBLIC_SCOPE=$NEXT_PUBLIC_SCOPE"
|
||||||
|
RUN echo "NEXT_PUBLIC_AUTHORITY=$NEXT_PUBLIC_AUTHORITY"
|
||||||
|
RUN echo "NEXT_PUBLIC_LOGOUT_URI=$NEXT_PUBLIC_LOGOUT_URI"
|
||||||
|
RUN echo "NEXT_PUBLIC_ACCESS_AS_USER=$NEXT_PUBLIC_ACCESS_AS_USER"
|
||||||
|
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# If using npm comment out above and use below instead
|
||||||
|
# RUN npm run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||||
|
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT 3000
|
||||||
|
ENV HOSTNAME 0.0.0.0
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_API_URL
|
||||||
|
ARG NEXT_PUBLIC_CERBEROS_API_URL
|
||||||
|
ARG NEXT_PUBLIC_APP_ID
|
||||||
|
ARG NEXT_PUBLIC_REDIRECT_URI
|
||||||
|
ARG NEXT_PUBLIC_SCOPE
|
||||||
|
ARG NEXT_PUBLIC_AUTHORITY
|
||||||
|
ARG NEXT_PUBLIC_LOGOUT_URI
|
||||||
|
ARG NEXT_PUBLIC_ACCESS_AS_USER
|
||||||
|
|
||||||
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||||
|
ENV NEXT_PUBLIC_CERBEROS_API_URL=$NEXT_PUBLIC_CERBEROS_API_URL
|
||||||
|
ENV NEXT_PUBLIC_APP_ID=$NEXT_PUBLIC_APP_ID
|
||||||
|
ENV NEXT_PUBLIC_REDIRECT_URI=$NEXT_PUBLIC_REDIRECT_URI
|
||||||
|
ENV NEXT_PUBLIC_SCOPE=$NEXT_PUBLIC_SCOPE
|
||||||
|
ENV NEXT_PUBLIC_AUTHORITY=$NEXT_PUBLIC_AUTHORITY
|
||||||
|
ENV NEXT_PUBLIC_LOGOUT_URI=$NEXT_PUBLIC_LOGOUT_URI
|
||||||
|
ENV NEXT_PUBLIC_ACCESS_AS_USER=$NEXT_PUBLIC_ACCESS_AS_USER
|
||||||
|
|
||||||
|
RUN echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL"
|
||||||
|
RUN echo "NEXT_PUBLIC_CERBEROS_API_URL=$NEXT_PUBLIC_CERBEROS_API_URL"
|
||||||
|
RUN echo "NEXT_PUBLIC_APP_ID=$NEXT_PUBLIC_APP_ID"
|
||||||
|
RUN echo "NEXT_PUBLIC_REDIRECT_URI=$NEXT_PUBLIC_REDIRECT_URI"
|
||||||
|
RUN echo "NEXT_PUBLIC_SCOPE=$NEXT_PUBLIC_SCOPE"
|
||||||
|
RUN echo "NEXT_PUBLIC_AUTHORITY=$NEXT_PUBLIC_AUTHORITY"
|
||||||
|
RUN echo "NEXT_PUBLIC_LOGOUT_URI=$NEXT_PUBLIC_LOGOUT_URI"
|
||||||
|
RUN echo "NEXT_PUBLIC_ACCESS_AS_USER=$NEXT_PUBLIC_ACCESS_AS_USER"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Introduction
|
||||||
|
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
|
||||||
|
1. Installation process
|
||||||
|
2. Software dependencies
|
||||||
|
3. Latest releases
|
||||||
|
4. API references
|
||||||
|
|
||||||
|
# Build and Test
|
||||||
|
TODO: Describe and show how to build your code and run the tests.
|
||||||
|
|
||||||
|
# Contribute
|
||||||
|
TODO: Explain how other users and developers can contribute to make your code better.
|
||||||
|
|
||||||
|
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
|
||||||
|
- [ASP.NET Core](https://github.com/aspnet/Home)
|
||||||
|
- [Visual Studio Code](https://github.com/Microsoft/vscode)
|
||||||
|
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
|
||||||
6
next.config.mjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
5526
package-lock.json
generated
Normal file
42
package.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "blueprint.ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^3.23.0",
|
||||||
|
"@headlessui/react": "^2.1.2",
|
||||||
|
"@heroicons/react": "^2.1.5",
|
||||||
|
"@types/material-ui": "^0.21.17",
|
||||||
|
"axios": "^1.8.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"msal": "^1.4.18",
|
||||||
|
"nanoid": "^3.3.8",
|
||||||
|
"next": "^14.2.26",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-router-dom": "^6.26.2",
|
||||||
|
"react-toastify": "^10.0.5",
|
||||||
|
"sass": "^1.77.8",
|
||||||
|
"swr": "^2.2.5",
|
||||||
|
"zustand": "^5.0.0-rc.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/jwt-decode": "^2.2.1",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.5",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
4
public/images/avatar.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="32" height="32" rx="16" fill="#BDBDBD"/>
|
||||||
|
<path d="M16 16C17.8416 16 19.3333 14.5083 19.3333 12.6666C19.3333 10.825 17.8416 9.33331 16 9.33331C14.1583 9.33331 12.6666 10.825 12.6666 12.6666C12.6666 14.5083 14.1583 16 16 16ZM16 17.6666C13.775 17.6666 9.33331 18.7833 9.33331 21V22.6666H22.6666V21C22.6666 18.7833 18.225 17.6666 16 17.6666Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||||
|
After Width: | Height: | Size: 629 B |
BIN
src/app/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/app/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
src/app/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/app/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 718 B |
BIN
src/app/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
383
src/app/globals.css
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--white-BP: #FFF;
|
||||||
|
--white-BP-rgb: 255, 255, 255;
|
||||||
|
--black-BP: #000;
|
||||||
|
--black-BP: 0,0,0;
|
||||||
|
--TextColor-BP-rgb: 5, 87, 201;
|
||||||
|
--background-BP-rgb: 255, 255, 255;
|
||||||
|
--primeBlue-BP: #0557C9;
|
||||||
|
--primeBlue-BP-alpha40: #0557C966;
|
||||||
|
--brightBlue-BP: #09F;
|
||||||
|
--brightBlue-BP-alpha40: #09F6;
|
||||||
|
--planeBorder-BP: #75C8FF;
|
||||||
|
--planeYellow-BP: #eab308;
|
||||||
|
--lightblue-BP: #a9ddff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--white-BP: #FFF;
|
||||||
|
--white-BP-rgb: 255, 255, 255;
|
||||||
|
--black-BP: #000;
|
||||||
|
--black-BP-rgb: 0,0,0;
|
||||||
|
--TextColor-BP-rgb: 255, 255, 255;
|
||||||
|
--background-BP-rgb: 0, 31, 71;
|
||||||
|
--primeBlue-BP: #002251;
|
||||||
|
--primeBlue-BP-alpha40: #00225166;
|
||||||
|
--brightBlue-BP: #013071;
|
||||||
|
--brightBlue-BP-alpha40: #01307166;
|
||||||
|
--planeBorder-BP: #0557C9;
|
||||||
|
--planeYellow-BP: #eab308;
|
||||||
|
--lightblue-BP: #4f93f3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html{
|
||||||
|
margin: 3rem;
|
||||||
|
background-color: rgb(var(--background-BP-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
Padding: 2rem;
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: calc(100vh - 6rem);
|
||||||
|
box-shadow: 4px 4px 8px 0px rgba(5, 87, 201, 0.45);
|
||||||
|
background: linear-gradient(116deg, var(--primeBlue-BP) 9.12%, var(--brightBlue-BP) 82.85%);
|
||||||
|
border: 1px solid var(--planeBorder-BP);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select{
|
||||||
|
color: var(--TextColor-BP-rgb);
|
||||||
|
background-color: var(--primeBlue-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper{
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
color: var(--white-BP);
|
||||||
|
position: relative;
|
||||||
|
border: 3px solid var(--white-BP);
|
||||||
|
min-height: calc(100vh - 11rem);
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
rgba(var(--white-BP-rgb), 0.5) 1%,
|
||||||
|
transparent 1%,
|
||||||
|
transparent 25%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 25%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 26%,
|
||||||
|
transparent 26%,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.3) 50%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.3) 51%,
|
||||||
|
transparent 51%,
|
||||||
|
transparent 75%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 75%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 76%,
|
||||||
|
transparent 76%,
|
||||||
|
transparent 100%),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(var(--white-BP-rgb), 0.5) 1%,
|
||||||
|
transparent 1%,
|
||||||
|
transparent 25%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 25%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 26%,
|
||||||
|
transparent 26%,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.3) 50%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.3) 51%,
|
||||||
|
transparent 51%,
|
||||||
|
transparent 75%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 75%,
|
||||||
|
rgba(var(--white-BP-rgb), 0.1) 76%,
|
||||||
|
transparent 76%,
|
||||||
|
transparent 100%);
|
||||||
|
background-size: 64px 64px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper::after{
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 330px;
|
||||||
|
height: 52px;
|
||||||
|
background-image: url(./images/UpstartSketch.svg);
|
||||||
|
float: left;
|
||||||
|
margin-top: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper > header{
|
||||||
|
margin: 1rem;
|
||||||
|
border: 1px solid var(--white-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper > header > div > div{
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper > header h1{
|
||||||
|
font-size: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper > header h3{
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerTools{
|
||||||
|
color: var(--TextColor-BP-rgb);
|
||||||
|
background: var(--brightBlue-BP);
|
||||||
|
border: 1px solid var(--white-BP);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ImageLoadButton{
|
||||||
|
background-color: var(--planeYellow-BP);
|
||||||
|
border: 3px solid var(--planeYellow-BP);
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-left-color: transparent;
|
||||||
|
padding: 6px;
|
||||||
|
background-clip:content-box;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
main{
|
||||||
|
min-height: calc(55vh);
|
||||||
|
padding: 48px;
|
||||||
|
background: url(./images/U-sketch.svg) top right no-repeat, url(./images/cota-sketch.svg) center left no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>div{
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--white-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
#container,
|
||||||
|
#formContainer{
|
||||||
|
width: auto;
|
||||||
|
min-width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
border-width: 3px !important;
|
||||||
|
border-style: dashed !important;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button{
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
background: url(./images/buttondecker.svg);
|
||||||
|
background-size: 100% 100%;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button::before{
|
||||||
|
content:"";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
margin: auto;
|
||||||
|
left: 15%;
|
||||||
|
top: 15%;
|
||||||
|
width: 70%;
|
||||||
|
height: 65%;
|
||||||
|
border: 2px dashed var(--white-BP);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button.editBtn{
|
||||||
|
filter: hue-rotate(142deg) saturate(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button.deleteBtn{
|
||||||
|
filter: hue-rotate(290deg) saturate(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button.saveBtn{
|
||||||
|
color: #FFF;
|
||||||
|
background-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button.saveBtn::before{
|
||||||
|
border-color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .button.cancelBtn{
|
||||||
|
filter: hue-rotate(290deg) saturate(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigationButton{
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 2.5vh;
|
||||||
|
padding: 84px 64px 42px 32px;
|
||||||
|
background: url(./images/Sketch-bg.svg);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
table::after,
|
||||||
|
table::before{
|
||||||
|
content:"";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
border-width: 2px;
|
||||||
|
border-top-style: dotted;
|
||||||
|
border-bottom: none;
|
||||||
|
border-color: var(--white-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
table::after{
|
||||||
|
border-left-style: dotted;
|
||||||
|
border-right: none;
|
||||||
|
right: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table::before{
|
||||||
|
border-right-style: dotted;
|
||||||
|
border-left: none;
|
||||||
|
left: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead{
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 2px dashed var(--white-BP);
|
||||||
|
background: var(--primeBlue-BP-alpha40);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead::after,
|
||||||
|
thead::before{
|
||||||
|
content:"";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: -32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
border-width: 2px;
|
||||||
|
border-bottom-style: dotted;
|
||||||
|
border-top: none;
|
||||||
|
border-color: var(--white-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead::after{
|
||||||
|
border-left-style: dotted;
|
||||||
|
border-right: none;
|
||||||
|
right: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead::before{
|
||||||
|
border-right-style: dotted;
|
||||||
|
border-left: none;
|
||||||
|
left: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-child(1){
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2), td:nth-child(3){
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child, td:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr{
|
||||||
|
background: var(--primeBlue-BP-alpha40);
|
||||||
|
border-bottom: 1px dashed var(--planeBorder-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#wrapper > footer {
|
||||||
|
float: right;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 1rem;
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid var(--white-BP);
|
||||||
|
background: var(--brightBlue-BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper > footer *{
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainMenu{
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuBtn{
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--white-BP);
|
||||||
|
display: block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuBtn span{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconOpenMenu{
|
||||||
|
background-image: url('../components/ui/icons/open-menu-icon.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fix icon */
|
||||||
|
.IconCloseMenu{
|
||||||
|
background-image: url('../components/ui/icons/close-menu-icon.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainMenu nav{
|
||||||
|
border: 3px solid var(--white-BP);
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainMenu nav a{
|
||||||
|
display: block;
|
||||||
|
padding: 12px 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--lightblue-BP);
|
||||||
|
border-bottom: 2px dashed var(--planeBorder-BP) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainMenu nav a:hover{
|
||||||
|
color: var(--white-BP);
|
||||||
|
}
|
||||||
67
src/app/images/Sketch-bg.svg
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<svg width="614" height="224" viewBox="0 0 614 224" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_12_2)">
|
||||||
|
<path d="M545.06 67.02V202.89H12.24V67.02H545.06ZM547.34 64.74H9.95999V205.18H547.35V64.74H547.34Z" fill="white"/>
|
||||||
|
<path d="M547.34 64.74H319.12C316.6 64.74 314.56 66.78 314.56 69.3C314.56 71.82 316.6 73.86 319.12 73.86H547.34V64.73V64.74ZM205.01 64.74H9.95999V73.87V97.91C9.95999 100.43 12 102.47 14.52 102.47C17.04 102.47 19.08 100.43 19.08 97.91V73.87H205.01C207.53 73.87 209.57 71.83 209.57 69.31C209.57 66.79 207.53 64.75 205.01 64.75V64.74ZM245.02 196.04H16.8C14.28 196.04 12.24 198.08 12.24 200.6C12.24 203.12 14.28 205.16 16.8 205.16H245.02C247.54 205.16 249.58 203.12 249.58 200.6C249.58 198.08 247.54 196.04 245.02 196.04ZM542.78 160.6C540.26 160.6 538.22 162.64 538.22 165.16V196.03H359.13C356.61 196.03 354.57 198.07 354.57 200.59C354.57 203.11 356.61 205.15 359.13 205.15H547.35V165.15C547.35 162.63 545.31 160.59 542.79 160.59L542.78 160.6Z" fill="white"/>
|
||||||
|
<path d="M514.65 211.41L540.44 166.75" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M503.24 211.41L549.74 130.87" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M491.83 211.41L543.4 122.09" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3.44 117.84L29.22 73.18" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M1.16 142.94L47.65 62.41" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M1.16 165.77L52.73 76.44" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M477.95 212.27L553.51 81.4" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M466.74 211.92L546.29 74.14" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M455.53 211.57L548.75 50.11" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M410.7 210.17L486.25 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M399.49 209.82L479.03 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M376.46 210.17L452.02 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M365.25 209.82L444.8 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M353.64 210.17L429.2 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M342.43 209.82L421.98 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M330.82 210.17L406.37 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M319.61 209.82L399.15 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M308 210.17L383.55 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M296.79 209.82L376.33 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M285.17 210.17L360.73 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M273.96 209.82L353.51 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M388.28 209.47L481.5 48.01" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M444.32 211.22L534.28 55.41" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M433.11 210.87L515.73 67.77" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M421.9 210.52L510.25 57.5" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M262.15 210.52L350.5 57.5" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M251.14 209.82L330.69 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M239.53 210.17L315.09 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M228.32 209.82L307.87 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M216.71 210.17L292.26 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M205.5 209.82L285.04 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M193.89 210.17L269.44 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M182.68 209.82L262.22 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M170.86 210.52L259.21 57.5" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M159.85 209.82L239.4 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M148.24 210.17L223.8 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M137.03 209.82L216.58 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M125.42 210.17L200.98 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M114.21 209.82L193.75 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M102.6 210.17L178.15 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M91.39 209.82L170.93 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M79.57 210.52L167.92 57.5" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M68.57 209.82L148.11 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M56.95 210.17L132.51 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M45.74 209.82L125.29 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M34.13 210.17L109.69 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M22.92 209.82L102.47 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11.31 210.17L86.87 79.3" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M0.100006 209.82L79.64 72.05" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9.95999 172.98L76.63 57.5" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M568.07 198.29V70.52" stroke="white" stroke-width="1.14" stroke-miterlimit="10"/>
|
||||||
|
<path d="M568.07 205.17C569.27 201.93 571.32 197.91 573.5 195.42L568.07 197.38L562.64 195.42C564.81 197.91 566.86 201.93 568.07 205.17Z" fill="white"/>
|
||||||
|
<path d="M568.07 63.63C569.27 66.87 571.32 70.89 573.5 73.38L568.07 71.42L562.64 73.38C564.81 70.89 566.86 66.87 568.07 63.63Z" fill="white"/>
|
||||||
|
<path d="M613.18 64.74H509.69" stroke="white" stroke-width="1.12" stroke-miterlimit="10" stroke-dasharray="5.61 5.61"/>
|
||||||
|
<path d="M613.18 204.03H509.69" stroke="white" stroke-width="1.12" stroke-miterlimit="10" stroke-dasharray="5.61 5.61"/>
|
||||||
|
<path d="M547.34 0V107.22" stroke="white" stroke-width="1.14" stroke-miterlimit="10" stroke-dasharray="5.71 5.71"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_12_2">
|
||||||
|
<rect width="613.18" height="223.37" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.7 KiB |
25
src/app/images/U-sketch.svg
Normal file
|
After Width: | Height: | Size: 79 KiB |
20
src/app/images/UpstartSketch.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<svg width="334" height="53" viewBox="0 0 334 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M57.4639 30.5211L84.0788 15.1487" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M68.3546 34.001L116.346 6.28479" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M80.8514 35.5076L134.081 4.77808" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M106.334 30.5211L132.949 15.1487" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M117.225 33.1214L165.216 5.41284" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M132.337 33.7639L185.575 3.02673" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M154.333 30.5211L180.948 15.1487" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M218.912 30.5211L245.527 15.1487" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M236.365 30.5211L257.611 18.2538" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M253.818 30.5211L275.064 18.2538" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M165.224 33.1214L213.222 5.41284" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M197.513 33.1214L236.449 10.644" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M180.336 33.7639L233.573 3.02673" stroke="white" stroke-width="2.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M292.256 33.7638C260.028 12.6784 214.706 2.74371 165.56 2.74371C116.414 2.74371 69.3718 13.7721 35.7437 33.7638H292.256Z" stroke="white" stroke-width="4.56" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.2251 51.1214L54.216 23.4128" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 4"/>
|
||||||
|
<path d="M320.216 51.1214L272.225 23.4128" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 4"/>
|
||||||
|
<path d="M333.416 33.9963L278 33.9955" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 4"/>
|
||||||
|
<path d="M53.4156 33.9963L-2.00001 33.9955" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
65
src/app/images/buttondecker.svg
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" width="552" height="170" viewBox="0 0 552 170" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||||
|
<defs>
|
||||||
|
<style type="text/css">
|
||||||
|
.colorStroke{
|
||||||
|
stroke: #2E7D32;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<clipPath id="clip0_18_126">
|
||||||
|
<rect width="551.41" height="169.46" fill="colorStroke"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path="url(#clip0_18_126)">
|
||||||
|
<path d="M487.23 107.06L513.01 62.4" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M467.37 125L513.86 44.46" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M455.32 129.39L506.89 40.07" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M78.4199 107.06L104.2 62.4" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M39.54 125L86.04 44.46" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M27.4902 129.39L79.0702 40.07" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M471.85 150.16L547.41 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M431.83 153.62L511.37 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M415.48 165.46L508.7 4" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M386.28 150.16L461.84 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M374.78 153.62L454.33 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M357.76 150.16L433.32 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M346.26 153.62L425.81 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M338.75 150.16L414.3 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M327.25 153.62L406.79 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M319.73 150.16L395.29 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M308.23 153.62L387.78 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M300.72 150.16L376.28 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M289.22 153.62L368.76 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M281.7 150.16L357.26 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M270.2 153.62L349.75 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M358.44 165.46L451.66 4" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M407.61 162.63L497.56 6.82999" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M401.77 156.28L484.39 13.18" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M389.4 161.24L477.74 8.22" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M256.29 161.24L344.64 8.22" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M251.19 153.62L330.73 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M243.68 150.16L319.23 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M232.17 153.62L311.72 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M224.66 150.16L300.22 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M213.16 153.62L292.7 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M205.65 150.16L281.2 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M194.15 153.62L273.69 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M180.24 161.24L268.58 8.22" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M175.13 153.62L254.68 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M167.62 150.16L243.17 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M156.12 153.62L235.66 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M148.6 150.16L224.16 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M137.1 153.62L216.65 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M129.59 150.16L205.14 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M118.09 153.62L197.63 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M104.18 161.24L192.53 8.22" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M99.0698 153.62L178.62 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M91.5601 150.16L167.12 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M80.0601 153.62L159.6 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M72.54 150.16L148.1 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M61.04 153.62L140.59 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M44.02 150.16L119.58 19.3" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M4 153.62L83.54 15.84" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M38.96 142.47L105.63 26.99" class="colorStroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
17
src/app/images/cota-sketch.svg
Normal file
|
After Width: | Height: | Size: 84 KiB |
38
src/app/layout.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Overpass_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import Header from "@/components/header";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import HeaderImage from "@/components/headerImage";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import SideNav from "@/components/side-nav/side-nav";
|
||||||
|
|
||||||
|
const overpass_mono = Overpass_Mono({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blueprint Sample",
|
||||||
|
description: "Blueprint Sample Application",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={`flex ${overpass_mono.className}`}>
|
||||||
|
<SideNav></SideNav>
|
||||||
|
<div id="wrapper">
|
||||||
|
<Header></Header>
|
||||||
|
<main className="flex flex-col items-center justify-between">
|
||||||
|
{children}
|
||||||
|
<ToastContainer />
|
||||||
|
</main>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
src/app/login-styles.module.css
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/* html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
font-family: 'Rubik', sans-serif;
|
||||||
|
font-family: 'Titillium Web', sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
} */
|
||||||
|
.w-100{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.h-100{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.d-flex {
|
||||||
|
display: flex!important;
|
||||||
|
}
|
||||||
|
.p25{
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
}
|
||||||
|
.align-items-end {
|
||||||
|
align-items: flex-end!important;
|
||||||
|
}
|
||||||
|
/* label {
|
||||||
|
display: inline-block;
|
||||||
|
} */
|
||||||
|
.form-label {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.form-control {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
height: 50px;
|
||||||
|
max-height: 50px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000000;
|
||||||
|
background: #FFF;
|
||||||
|
background-color:#FFF;
|
||||||
|
border: 1px solid #FFF;
|
||||||
|
border-radius: 15px;
|
||||||
|
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||||
|
}
|
||||||
|
.form-group{
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.btnLoginMicrosoft {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: gray;
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid #0b335b;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn{
|
||||||
|
background-color: #0b335b; /* Green */
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 100%;
|
||||||
|
cursor:pointer
|
||||||
|
}
|
||||||
|
.btn-ouline{
|
||||||
|
background-color: #FFF !important;
|
||||||
|
border: 2px solid #0b335b !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
.split {
|
||||||
|
height: 100%;
|
||||||
|
width: 50%;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
left: 0;
|
||||||
|
background-color: #f7f7fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
right: 0;
|
||||||
|
/* background-image: url(../../../assets/img/heathCarrouselbg.png); */
|
||||||
|
background-position: center center;
|
||||||
|
background-size:cover ;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 200px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.login-form{
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.text-login-image{
|
||||||
|
position: absolute;
|
||||||
|
top: 70%;
|
||||||
|
left: 5%;
|
||||||
|
}
|
||||||
|
.text-login-image h2{
|
||||||
|
color: #000;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
letter-spacing: 0.75px;
|
||||||
|
}
|
||||||
|
.text-login-form h2{
|
||||||
|
font-size: 35px ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container{
|
||||||
|
width: auto;
|
||||||
|
min-width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
border-width: 3px !important;
|
||||||
|
border-style: dashed !important;
|
||||||
|
border-left: none;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container-inner{
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
5
src/app/main/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Main() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
269
src/app/module/[id]/page.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createModule, updateModule, useModuleItem } from "@/hooks/module";
|
||||||
|
import { Module } from "@/lib/interfaces";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Applications, Status } from "@/lib/Enums";
|
||||||
|
|
||||||
|
export default function ModuleDetails() {
|
||||||
|
const [item, setItem] = useState<Module>();
|
||||||
|
const pathName = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
let isUpdateMode = false;
|
||||||
|
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
new Intl.DateTimeFormat("en-US").format(date);
|
||||||
|
|
||||||
|
const StatusDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value="Active">{Status.Active}</option>
|
||||||
|
<option value="Inactive">{Status.Inactive}</option>
|
||||||
|
<option value="Deleted">{Status.Deleted}</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApplicationsDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="application"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value={Applications.BluePrint}>{Applications.BluePrint}</option>
|
||||||
|
<option value={Applications.CustomerDashboard}>
|
||||||
|
{Applications.CustomerDashboard}
|
||||||
|
</option>
|
||||||
|
<option value={Applications.Discover}>{Applications.Discover}</option>
|
||||||
|
<option value={Applications.LSAMobile}>{Applications.LSAMobile}</option>
|
||||||
|
<option value={Applications.LSAWebPortal}>
|
||||||
|
{Applications.LSAWebPortal}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
let id = (document.getElementById("id") as HTMLInputElement)?.value;
|
||||||
|
let name = (document.getElementById("name") as HTMLInputElement)?.value;
|
||||||
|
let description = (
|
||||||
|
document.getElementById("description") as HTMLInputElement
|
||||||
|
)?.value;
|
||||||
|
let icon = (document.getElementById("icon") as HTMLInputElement)?.value;
|
||||||
|
let route = (document.getElementById("route") as HTMLInputElement)?.value;
|
||||||
|
let orderText = (document.getElementById("order") as HTMLInputElement)
|
||||||
|
?.value;
|
||||||
|
let order = parseInt(orderText, 10) ? parseInt(orderText, 10) : 0;
|
||||||
|
let application = (
|
||||||
|
document.getElementById("application") as HTMLSelectElement
|
||||||
|
)?.value;
|
||||||
|
let status = (document.getElementById("status") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
|
||||||
|
let updatedModule = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
route,
|
||||||
|
order,
|
||||||
|
application,
|
||||||
|
status,
|
||||||
|
} as unknown as Module;
|
||||||
|
if (isUpdateMode) {
|
||||||
|
updateModule(updatedModule);
|
||||||
|
} else {
|
||||||
|
updatedModule.id = null;
|
||||||
|
createModule(updatedModule);
|
||||||
|
}
|
||||||
|
setItem(updatedModule);
|
||||||
|
router.push("/module");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = (module: Module) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Id:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="id"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={module.id ?? ""}
|
||||||
|
readOnly
|
||||||
|
placeholder={module.id ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Name:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={module.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Description:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="description"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={module.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Application:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<ApplicationsDropDown
|
||||||
|
statusCode={module.application}
|
||||||
|
></ApplicationsDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Route:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="route"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={module.route}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Order:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="order"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="number"
|
||||||
|
defaultValue={module.order ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Icon:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="icon"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={module.icon ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(module.createdAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={module.createdBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(module.updatedAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={module.updatedBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Status:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<StatusDropDown statusCode={module.status}></StatusDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemData = () => {
|
||||||
|
const id = pathName.split("/");
|
||||||
|
const idValue = id[id.length - 1];
|
||||||
|
isUpdateMode = idValue != "create";
|
||||||
|
if (isUpdateMode) {
|
||||||
|
const { item, isLoading, error } = useModuleItem(idValue);
|
||||||
|
if (!isLoading && !error && item) {
|
||||||
|
return renderForm(item);
|
||||||
|
} else return <>Loading</>;
|
||||||
|
} else {
|
||||||
|
return renderForm({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
route: "",
|
||||||
|
order: null,
|
||||||
|
icon: "",
|
||||||
|
application: Applications.BluePrint,
|
||||||
|
status: Status.Active,
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdBy: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="formContainer" className="flex flex-col">
|
||||||
|
<div>
|
||||||
|
<ItemData></ItemData>
|
||||||
|
<div className="flex flex-row justify-center pb-10">
|
||||||
|
<a onClick={handleSaveClick} className="button saveBtn">
|
||||||
|
Save
|
||||||
|
</a>
|
||||||
|
<Link className="button cancelBtn" href="../module">
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/app/module/page.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useModuleItems, ChangeModuleStatus } from "@/hooks/module";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Loader from "../../components/ui/loader.svg";
|
||||||
|
import { ChangeStatusRequest } from "@/lib/interfaces";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
|
||||||
|
export default function Module() {
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
Intl.DateTimeFormat("en-US").format(new Date(date));
|
||||||
|
let refreshData: any;
|
||||||
|
|
||||||
|
const DeleteButton = (itemId: any) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="button deleteBtn"
|
||||||
|
value="Delete"
|
||||||
|
onClick={async () => {
|
||||||
|
const statusRequest: ChangeStatusRequest = {
|
||||||
|
id: itemId.itemId,
|
||||||
|
status: Status.Deleted,
|
||||||
|
};
|
||||||
|
await ChangeModuleStatus(statusRequest);
|
||||||
|
refreshData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemsData = () => {
|
||||||
|
const { items, error, isLoading, mutate } = useModuleItems();
|
||||||
|
refreshData = mutate;
|
||||||
|
if (!isLoading && !error && items != null) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<Link href={`module/${item.id}`}>{item.name}</Link>
|
||||||
|
</td>
|
||||||
|
<td>{item.description}</td>
|
||||||
|
<td>{item.route}</td>
|
||||||
|
<td>{item.application}</td>
|
||||||
|
<td>{item.order}</td>
|
||||||
|
<td>{item.status}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex flex-nowrap">
|
||||||
|
<Link className="button editBtn" href={`module/${item.id}`}>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<DeleteButton itemId={item.id}></DeleteButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
} else return Loading();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = () => (
|
||||||
|
<tr>
|
||||||
|
<td className="flex align-items-end" colSpan={5}>
|
||||||
|
<Image src={Loader} alt="Loading" width={62} />
|
||||||
|
Loading
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div id="container" className="flex align-items-start place-content-center">
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={5}>
|
||||||
|
<Link className="button" href={`module/create`}>
|
||||||
|
<span>Add new</span>
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Route</th>
|
||||||
|
<th>Application</th>
|
||||||
|
<th>Order</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Suspense fallback={Loading()}>
|
||||||
|
<ItemsData></ItemsData>
|
||||||
|
</Suspense>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{process.env.PUBLIC_CERBEROS_API_URL}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/app/onboarding/page.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Onboarding() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link id="navigationButton" href="user">
|
||||||
|
Go to User CRUD Page
|
||||||
|
</Link>
|
||||||
|
<Link id="navigationButton" href="role">
|
||||||
|
Go to Role CRUD Page
|
||||||
|
</Link>
|
||||||
|
<Link id="navigationButton" href="permission">
|
||||||
|
Go to Permission Page
|
||||||
|
</Link>
|
||||||
|
<Link id="navigationButton" href="module">
|
||||||
|
Go to Module Page
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
src/app/page.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styles from "./login-styles.module.css";
|
||||||
|
import msalInstance from "../lib/msalInstance";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { setCookie } from "@/lib/AuthCookie";
|
||||||
|
import { AccountInfo } from "@azure/msal-browser";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { UserClaims } from "@/lib/interfaces";
|
||||||
|
import { Applications } from "@/lib/Enums";
|
||||||
|
import useUserFromCookie from "@/lib/useUser";
|
||||||
|
import { validateExistance } from "@/hooks/user";
|
||||||
|
import { getAccessToken } from "@/services/cerberos-services";
|
||||||
|
|
||||||
|
interface GroupsMe {
|
||||||
|
id: number;
|
||||||
|
nameGroup: string;
|
||||||
|
uidGroup: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const groups: GroupsMe[] = [];
|
||||||
|
const route = useRouter();
|
||||||
|
const { userData, setUser } = useUserFromCookie({ redirectTo: "/" });
|
||||||
|
const appId = Applications.BluePrint;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkLoginStatus = async () => {
|
||||||
|
await msalInstance.initialize();
|
||||||
|
await msalInstance.handleRedirectPromise();
|
||||||
|
};
|
||||||
|
checkLoginStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const warningAlert = async (message: any) => {
|
||||||
|
toast.warn(message, {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
const response = validateExistance(email);
|
||||||
|
if (response && (await response).existence) {
|
||||||
|
handleMicrosoftLogin(email);
|
||||||
|
} else {
|
||||||
|
warningAlert("User not found or invalid user account");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMicrosoftLogin = async (email: any) => {
|
||||||
|
try {
|
||||||
|
const loginResponse = await msalInstance.loginPopup();
|
||||||
|
const accessToken = loginResponse.accessToken;
|
||||||
|
const accounts = msalInstance.getAllAccounts();
|
||||||
|
|
||||||
|
const accessTokenRequest = {
|
||||||
|
scopes: [`${process.env.NEXT_PUBLIC_ACCESS_AS_USER}`],
|
||||||
|
account: msalInstance.getAllAccounts()[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
const callProtectedApi = async () => {
|
||||||
|
const response = await msalInstance.acquireTokenSilent(
|
||||||
|
accessTokenRequest
|
||||||
|
);
|
||||||
|
var user = await getAccessToken(response.accessToken);
|
||||||
|
if (user) {
|
||||||
|
const decoded = jwt.decode(user.token ?? "") as UserClaims;
|
||||||
|
|
||||||
|
if (!decoded.applications.includes(appId)) {
|
||||||
|
warningAlert("You do not have access to this application");
|
||||||
|
} else {
|
||||||
|
setCookie("authToken", (await user).token ?? "", 1);
|
||||||
|
const userData = {
|
||||||
|
name: user.displayName,
|
||||||
|
email: user.email,
|
||||||
|
roleId: user.roleId,
|
||||||
|
roleName: user.roleId,
|
||||||
|
lastLogin: user.lastLogIn ? user.lastLogIn : null,
|
||||||
|
lastLogout: user.lastLogOut ? user.lastLogOut : null,
|
||||||
|
modules: decoded.modules.filter(
|
||||||
|
(module) => module.application === appId
|
||||||
|
),
|
||||||
|
};
|
||||||
|
setCookie("user", JSON.stringify(userData), 1);
|
||||||
|
setUser(userData);
|
||||||
|
route.push("/main");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
callProtectedApi();
|
||||||
|
const activeAccount: AccountInfo | null = accounts ? accounts[0] : null;
|
||||||
|
let groupsOfAD = [];
|
||||||
|
let rolsOfAD = [];
|
||||||
|
if (activeAccount) {
|
||||||
|
groupsOfAD = Object(activeAccount.idTokenClaims?.groups) ?? [];
|
||||||
|
rolsOfAD = Object(activeAccount.idTokenClaims?.roles) ?? [];
|
||||||
|
const rolesJson = JSON.stringify(rolsOfAD);
|
||||||
|
localStorage.setItem("roles", rolesJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoggedIn(accounts.length > 0);
|
||||||
|
const groupsByUser = await fetch(
|
||||||
|
"https://graph.microsoft.com/v1.0/me/memberOf",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (groupsByUser.ok) {
|
||||||
|
const groupsMe = await groupsByUser.json();
|
||||||
|
groupsOfAD.map((group: any) => {
|
||||||
|
let mapGroup = groupsMe.value.find(
|
||||||
|
(groupMe: any) => groupMe.id === group
|
||||||
|
);
|
||||||
|
groups.push({
|
||||||
|
id: mapGroup.id,
|
||||||
|
nameGroup: mapGroup.displayName,
|
||||||
|
uidGroup: mapGroup.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const groupsJson = JSON.stringify(groups);
|
||||||
|
localStorage.setItem("groups", groupsJson);
|
||||||
|
} else {
|
||||||
|
setIsAlertOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
route.push("/main");
|
||||||
|
} catch (error) {
|
||||||
|
setLoggedIn(false);
|
||||||
|
setIsAlertOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleEmailChange(e: any) {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isAlertOpen, setIsAlertOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles["login-container"]}>
|
||||||
|
<div className={styles["login-container-inner"]}>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
<input
|
||||||
|
className={styles["form-control"]}
|
||||||
|
value={email}
|
||||||
|
placeholder="Email"
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="button" type="button" onClick={handleLogin}>
|
||||||
|
Log in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
224
src/app/permission/[id]/page.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createPermission,
|
||||||
|
updatePermission,
|
||||||
|
usePermissionItem,
|
||||||
|
} from "@/hooks/permission";
|
||||||
|
import { Permission } from "@/lib/interfaces";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
import { AccessLevel } from "@/lib/Enums";
|
||||||
|
|
||||||
|
export default function PermissionDetails() {
|
||||||
|
const [item, setItem] = useState<Permission>();
|
||||||
|
const pathName = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
let isUpdateMode = false;
|
||||||
|
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
new Intl.DateTimeFormat("en-US").format(date);
|
||||||
|
|
||||||
|
const StatusDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value="Active">{Status.Active}</option>
|
||||||
|
<option value="Inactive">{Status.Inactive}</option>
|
||||||
|
<option value="Deleted">{Status.Deleted}</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccessLevelDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="accessLevel"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value={AccessLevel.Read}>{AccessLevel.Read}</option>
|
||||||
|
<option value={AccessLevel.Write}>{AccessLevel.Write}</option>
|
||||||
|
<option value={AccessLevel.All}>{AccessLevel.All}</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
let id = (document.getElementById("id") as HTMLInputElement)?.value;
|
||||||
|
let name = (document.getElementById("name") as HTMLInputElement)?.value;
|
||||||
|
let description = (
|
||||||
|
document.getElementById("description") as HTMLInputElement
|
||||||
|
)?.value;
|
||||||
|
let accessLevel = (
|
||||||
|
document.getElementById("accessLevel") as HTMLSelectElement
|
||||||
|
)?.value;
|
||||||
|
let status = (document.getElementById("status") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
|
||||||
|
let updatedPermission = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
accessLevel,
|
||||||
|
status,
|
||||||
|
} as unknown as Permission;
|
||||||
|
if (isUpdateMode) {
|
||||||
|
updatePermission(updatedPermission);
|
||||||
|
} else {
|
||||||
|
updatedPermission.id = null;
|
||||||
|
createPermission(updatedPermission);
|
||||||
|
}
|
||||||
|
setItem(updatedPermission);
|
||||||
|
router.push("/permission");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = (permission: Permission) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Id:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="id"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={permission.id ?? ""}
|
||||||
|
readOnly
|
||||||
|
placeholder={permission.id ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Name:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={permission.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Description:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="description"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={permission.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>AccessLevel:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<AccessLevelDropDown
|
||||||
|
statusCode={permission.accessLevel}
|
||||||
|
></AccessLevelDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(permission.createdAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={permission.createdBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(permission.updatedAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={permission.updatedBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Status:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<StatusDropDown statusCode={permission.status}></StatusDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemData = () => {
|
||||||
|
const id = pathName.split("/");
|
||||||
|
const idValue = id[id.length - 1];
|
||||||
|
isUpdateMode = idValue != "create";
|
||||||
|
if (isUpdateMode) {
|
||||||
|
const { item, isLoading, error } = usePermissionItem(idValue);
|
||||||
|
if (!isLoading && !error && item) {
|
||||||
|
return renderForm(item);
|
||||||
|
} else return <>Loading</>;
|
||||||
|
} else {
|
||||||
|
return renderForm({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
accessLevel: "",
|
||||||
|
status: Status.Active,
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdBy: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="formContainer" className="flex flex-col">
|
||||||
|
<div>
|
||||||
|
<ItemData></ItemData>
|
||||||
|
<div className="flex flex-row justify-center pb-10">
|
||||||
|
<a onClick={handleSaveClick} className="button saveBtn">
|
||||||
|
Save
|
||||||
|
</a>
|
||||||
|
<Link className="button cancelBtn" href="../permission">
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/app/permission/page.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePermissionItems, ChangePermissionStatus } from "@/hooks/permission";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Loader from "../../components/ui/loader.svg";
|
||||||
|
import { ChangeStatusRequest } from "@/lib/interfaces";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
import { stat } from "fs";
|
||||||
|
|
||||||
|
export default function Permission() {
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
Intl.DateTimeFormat("en-US").format(new Date(date));
|
||||||
|
let refreshData: any;
|
||||||
|
|
||||||
|
const DeleteButton = (itemId: any) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="button deleteBtn"
|
||||||
|
value="Delete"
|
||||||
|
onClick={async () => {
|
||||||
|
const statusRequest: ChangeStatusRequest = {
|
||||||
|
id: itemId.itemId,
|
||||||
|
status: Status.Deleted,
|
||||||
|
};
|
||||||
|
await ChangePermissionStatus(statusRequest);
|
||||||
|
refreshData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemsData = () => {
|
||||||
|
const { items, error, isLoading, mutate } = usePermissionItems();
|
||||||
|
refreshData = mutate;
|
||||||
|
if (!isLoading && !error && items != null) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<Link href={`permission/${item.id}`}>{item.name}</Link>
|
||||||
|
</td>
|
||||||
|
<td>{item.description}</td>
|
||||||
|
<td>{item.accessLevel}</td>
|
||||||
|
<td>{item.status}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex flex-nowrap">
|
||||||
|
<Link className="button editBtn" href={`permission/${item.id}`}>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<DeleteButton itemId={item.id}></DeleteButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
} else return Loading();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = () => (
|
||||||
|
<tr>
|
||||||
|
<td className="flex align-items-end" colSpan={5}>
|
||||||
|
<Image src={Loader} alt="Loading" width={62} />
|
||||||
|
Loading
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div id="container" className="flex align-items-start place-content-center">
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={5}>
|
||||||
|
<Link className="button" href={`permission/create`}>
|
||||||
|
<span>Add new</span>
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>AccessLevel</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Suspense fallback={Loading()}>
|
||||||
|
<ItemsData></ItemsData>
|
||||||
|
</Suspense>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{process.env.PUBLIC_CERBEROS_API_URL}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
385
src/app/role/[id]/page.tsx
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createRole, updateRole, useRoleItem } from "@/hooks/role";
|
||||||
|
import { Module, Permission, Role } from "@/lib/interfaces";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
import { Applications } from "@/lib/Enums";
|
||||||
|
import { usePermissionItems } from "@/hooks/permission";
|
||||||
|
import { useModuleItems } from "@/hooks/module";
|
||||||
|
|
||||||
|
export default function RoleDetails() {
|
||||||
|
const [item, setItem] = useState<Role>();
|
||||||
|
const pathName = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
let isUpdateMode = false;
|
||||||
|
let selectedApplications: string[];
|
||||||
|
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||||
|
const [modules, setModules] = useState<Module[]>([]);
|
||||||
|
let initialPermissions: string[];
|
||||||
|
let initialApplications: string[];
|
||||||
|
let initialModules: string[];
|
||||||
|
|
||||||
|
const setSelectedValuesGlobal = (values: string[]) => {
|
||||||
|
selectedApplications = values;
|
||||||
|
};
|
||||||
|
|
||||||
|
let selectedPermissions: string[];
|
||||||
|
const setSelectedPermissionsGlobal = (values: string[]) => {
|
||||||
|
selectedPermissions = values;
|
||||||
|
};
|
||||||
|
|
||||||
|
let selectedModules: string[];
|
||||||
|
const setSelectedModulesGlobal = (values: string[]) => {
|
||||||
|
selectedModules = values;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
new Intl.DateTimeFormat("en-US").format(date);
|
||||||
|
|
||||||
|
const StatusDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value={Status.Active}>{Status.Active}</option>
|
||||||
|
<option value={Status.Inactive}>{Status.Inactive}</option>
|
||||||
|
<option value={Status.Deleted}>{Status.Deleted}</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApplicationsDropDown = ({
|
||||||
|
selectedApplications,
|
||||||
|
}: {
|
||||||
|
selectedApplications: string[];
|
||||||
|
}) => {
|
||||||
|
const [localSelectedValues, setLocalSelectedValues] =
|
||||||
|
useState<string[]>(selectedApplications);
|
||||||
|
|
||||||
|
const handleSelection = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selectedOptions = Array.from(event.target.selectedOptions);
|
||||||
|
const values = selectedOptions.map((option) => option.value);
|
||||||
|
setLocalSelectedValues(values);
|
||||||
|
setSelectedValuesGlobal(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="applications"
|
||||||
|
className="p-3 rounded"
|
||||||
|
multiple
|
||||||
|
value={localSelectedValues}
|
||||||
|
onChange={handleSelection}
|
||||||
|
>
|
||||||
|
<option value={Applications.LSAWebPortal}>
|
||||||
|
{Applications.LSAWebPortal}
|
||||||
|
</option>
|
||||||
|
<option value={Applications.CustomerDashboard}>
|
||||||
|
CustomerDashboard
|
||||||
|
</option>
|
||||||
|
<option value={Applications.Discover}>{Applications.Discover}</option>
|
||||||
|
<option value={Applications.LSAMobile}>{Applications.LSAMobile}</option>
|
||||||
|
<option value={Applications.BluePrint}>{Applications.BluePrint}</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PermissionsDropDown = ({
|
||||||
|
selectedPermissions,
|
||||||
|
}: {
|
||||||
|
selectedPermissions: string[];
|
||||||
|
}) => {
|
||||||
|
const [localSelectedValues, setLocalSelectedValues] =
|
||||||
|
useState<string[]>(selectedPermissions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalSelectedValues(selectedPermissions);
|
||||||
|
}, [selectedPermissions]);
|
||||||
|
|
||||||
|
const handleSelection = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selectedOptions = Array.from(event.target.selectedOptions);
|
||||||
|
const values = selectedOptions.map((option) => option.value);
|
||||||
|
setLocalSelectedValues(values);
|
||||||
|
setSelectedPermissionsGlobal(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedPermissions) setSelectedPermissionsGlobal(selectedPermissions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="permissions"
|
||||||
|
className="p-3 rounded"
|
||||||
|
multiple
|
||||||
|
value={localSelectedValues}
|
||||||
|
onChange={handleSelection}
|
||||||
|
>
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<option key={permission.id} value={`${permission.id}`}>
|
||||||
|
{permission.name} ({permission.accessLevel})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModulesDropDown = ({
|
||||||
|
selectedModules,
|
||||||
|
}: {
|
||||||
|
selectedModules: string[];
|
||||||
|
}) => {
|
||||||
|
const [localSelectedValues, setLocalSelectedValues] =
|
||||||
|
useState<string[]>(selectedModules);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalSelectedValues(selectedModules);
|
||||||
|
}, [selectedModules]);
|
||||||
|
|
||||||
|
const handleSelection = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selectedOptions = Array.from(event.target.selectedOptions);
|
||||||
|
const values = selectedOptions.map((option) => option.value);
|
||||||
|
setLocalSelectedValues(values);
|
||||||
|
setSelectedModulesGlobal(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedModules) setSelectedModulesGlobal(selectedModules);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="modules"
|
||||||
|
className="p-3 rounded"
|
||||||
|
multiple
|
||||||
|
value={localSelectedValues}
|
||||||
|
onChange={handleSelection}
|
||||||
|
>
|
||||||
|
{modules.map((module) => (
|
||||||
|
<option key={module.id} value={`${module.id}`}>
|
||||||
|
{module.name} ({module.application})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
let id = (document.getElementById("id") as HTMLInputElement)?.value;
|
||||||
|
let name = (document.getElementById("name") as HTMLInputElement)?.value;
|
||||||
|
let description = (
|
||||||
|
document.getElementById("description") as HTMLInputElement
|
||||||
|
)?.value;
|
||||||
|
let applications = selectedApplications
|
||||||
|
? selectedApplications
|
||||||
|
: initialApplications;
|
||||||
|
let permissions = selectedPermissions
|
||||||
|
? selectedPermissions
|
||||||
|
: initialPermissions;
|
||||||
|
let modules = selectedModules ? selectedModules : initialModules;
|
||||||
|
let status = (document.getElementById("status") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
|
||||||
|
let updatedRole = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
applications,
|
||||||
|
permissions,
|
||||||
|
modules,
|
||||||
|
status,
|
||||||
|
} as unknown as Role;
|
||||||
|
if (isUpdateMode) {
|
||||||
|
updateRole(updatedRole);
|
||||||
|
} else {
|
||||||
|
updatedRole.id = null;
|
||||||
|
createRole(updatedRole);
|
||||||
|
}
|
||||||
|
setItem(updatedRole);
|
||||||
|
router.push("/role");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = (role: Role) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Id:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="id"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={role.id ?? ""}
|
||||||
|
readOnly
|
||||||
|
placeholder={role.id ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Name:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={role.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Description:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="description"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={role.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Applications:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<ApplicationsDropDown selectedApplications={role.applications} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Permissions:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<PermissionsDropDown selectedPermissions={role.permissions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Modules:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<ModulesDropDown selectedModules={role.modules} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(role.createdAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={role.createdBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(role.updatedAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={role.updatedBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Status:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<StatusDropDown statusCode={role.status}></StatusDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemData = () => {
|
||||||
|
const id = pathName.split("/");
|
||||||
|
const idValue = id[id.length - 1];
|
||||||
|
isUpdateMode = idValue != "create";
|
||||||
|
if (isUpdateMode) {
|
||||||
|
const { item, isLoading, error } = useRoleItem(idValue);
|
||||||
|
const {
|
||||||
|
items: permissionItems,
|
||||||
|
error: permissionError,
|
||||||
|
isLoading: isPermissionLoading,
|
||||||
|
mutate: mutatePermission,
|
||||||
|
} = usePermissionItems();
|
||||||
|
const {
|
||||||
|
items: moduleItems,
|
||||||
|
error: moduleError,
|
||||||
|
isLoading: isModuleLoading,
|
||||||
|
mutate: mutateModule,
|
||||||
|
} = useModuleItems();
|
||||||
|
initialApplications = item.applications;
|
||||||
|
initialPermissions = item.permissions;
|
||||||
|
initialModules = item.modules;
|
||||||
|
useEffect(() => {
|
||||||
|
if (permissionItems && permissionItems.length > 0) {
|
||||||
|
setPermissions(permissionItems); // Solo actualiza si hay cambios
|
||||||
|
}
|
||||||
|
}, [permissionItems]); // Solo se ejecuta si permissionItems cambia
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (moduleItems && moduleItems.length > 0) {
|
||||||
|
setModules(moduleItems); // Solo actualiza si hay cambios
|
||||||
|
}
|
||||||
|
}, [moduleItems]);
|
||||||
|
|
||||||
|
if (!isLoading && !error && item) {
|
||||||
|
return renderForm(item);
|
||||||
|
} else return <>Loading</>;
|
||||||
|
} else {
|
||||||
|
return renderForm({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
applications: [],
|
||||||
|
permissions: [],
|
||||||
|
modules: [],
|
||||||
|
status: Status.Active,
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdBy: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="formContainer" className="flex flex-col">
|
||||||
|
<div>
|
||||||
|
<ItemData></ItemData>
|
||||||
|
<div className="flex flex-row justify-center pb-10">
|
||||||
|
<a onClick={handleSaveClick} className="button saveBtn">
|
||||||
|
Save
|
||||||
|
</a>
|
||||||
|
<Link className="button cancelBtn" href="../role">
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
140
src/app/role/page.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRoleItems, ChangeRoleStatus } from "@/hooks/role";
|
||||||
|
import { usePermissionItems } from "@/hooks/permission";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Loader from "../../components/ui/loader.svg";
|
||||||
|
import { ChangeStatusRequest } from "@/lib/interfaces";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
import { stat } from "fs";
|
||||||
|
|
||||||
|
export default function Role() {
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
Intl.DateTimeFormat("en-US").format(new Date(date));
|
||||||
|
let refreshData: any;
|
||||||
|
|
||||||
|
const DeleteButton = (itemId: any) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="button deleteBtn"
|
||||||
|
value="Delete"
|
||||||
|
onClick={async () => {
|
||||||
|
const statusRequest: ChangeStatusRequest = {
|
||||||
|
id: itemId.itemId,
|
||||||
|
status: Status.Deleted,
|
||||||
|
};
|
||||||
|
await ChangeRoleStatus(statusRequest);
|
||||||
|
refreshData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemsData = () => {
|
||||||
|
const { items, error, isLoading, mutate } = useRoleItems();
|
||||||
|
const {
|
||||||
|
items: permissionItems,
|
||||||
|
error: permissionError,
|
||||||
|
isLoading: isPermissionLoading,
|
||||||
|
mutate: mutatePermission,
|
||||||
|
} = usePermissionItems();
|
||||||
|
|
||||||
|
const permissionMap = permissionItems?.reduce((acc, perm) => {
|
||||||
|
if (perm.id) {
|
||||||
|
acc[perm.id] = { name: perm.name, accessLevel: perm.accessLevel };
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, { name: string; accessLevel: string }>);
|
||||||
|
|
||||||
|
refreshData = mutate;
|
||||||
|
if (!isLoading && !error && items != null) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<Link href={`role/${item.id}`}>{item.name}</Link>
|
||||||
|
</td>
|
||||||
|
<td>{item.description}</td>
|
||||||
|
<td>
|
||||||
|
{item.applications?.map((permId, index) => {
|
||||||
|
const permission = permissionMap[permId];
|
||||||
|
return (
|
||||||
|
<span key={index}>
|
||||||
|
{permission
|
||||||
|
? `${permission.name} (${permission.accessLevel})`
|
||||||
|
: permId}{" "}
|
||||||
|
{index < item.applications.length - 1 && <br />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{item.permissions?.map((permId, index) => {
|
||||||
|
const application = permissionMap[permId];
|
||||||
|
return (
|
||||||
|
<span key={index}>
|
||||||
|
{application
|
||||||
|
? `${application.name} (${application.accessLevel})`
|
||||||
|
: permId}{" "}
|
||||||
|
{index < item.permissions.length - 1 && <br />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
<td>{item.status}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex flex-nowrap">
|
||||||
|
<Link className="button editBtn" href={`role/${item.id}`}>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<DeleteButton itemId={item.id}></DeleteButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
} else return Loading();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = () => (
|
||||||
|
<tr>
|
||||||
|
<td className="flex align-items-end" colSpan={5}>
|
||||||
|
<Image src={Loader} alt="Loading" width={62} />
|
||||||
|
Loading
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div id="container" className="flex align-items-start place-content-center">
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={5}>
|
||||||
|
<Link className="button" href={`role/create`}>
|
||||||
|
<span>Add new</span>
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Applications</th>
|
||||||
|
<th>Permissions</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Suspense fallback={Loading()}>
|
||||||
|
<ItemsData></ItemsData>
|
||||||
|
</Suspense>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{process.env.PUBLIC_CERBEROS_API_URL}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
src/app/sample/[id]/page.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createSampleItem,
|
||||||
|
updateSampleItem,
|
||||||
|
useSampleItem,
|
||||||
|
} from "@/hooks/use-sample-items";
|
||||||
|
import { SampleItem } from "@/lib/interfaces";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
|
export default function SampleItemDetails() {
|
||||||
|
const [item, setItem] = useState<SampleItem>();
|
||||||
|
const pathName = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
let isUpdateMode = false;
|
||||||
|
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
new Intl.DateTimeFormat("en-US").format(date);
|
||||||
|
|
||||||
|
const StatusDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value="A">Active</option>
|
||||||
|
<option value="P">Pending</option>
|
||||||
|
<option value="D">Deleted</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
let id = (document.getElementById("itemId") as HTMLInputElement)
|
||||||
|
?.value as UUID;
|
||||||
|
let testName = (document.getElementById("itemName") as HTMLInputElement)
|
||||||
|
?.value;
|
||||||
|
let statusCode = (document.getElementById("status") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
let numValue = (document.getElementById("numValue") as HTMLInputElement)
|
||||||
|
?.value;
|
||||||
|
let createdAt = item?.createdAt ?? new Date();
|
||||||
|
let updatedItem = {
|
||||||
|
id,
|
||||||
|
testName,
|
||||||
|
statusCode,
|
||||||
|
numValue,
|
||||||
|
createdAt,
|
||||||
|
} as unknown as SampleItem;
|
||||||
|
if (isUpdateMode) {
|
||||||
|
updateSampleItem(updatedItem);
|
||||||
|
} else {
|
||||||
|
updatedItem.id = null
|
||||||
|
createSampleItem(updatedItem);
|
||||||
|
}
|
||||||
|
setItem(updatedItem);
|
||||||
|
router.push("/sample");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = (item: SampleItem) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Id:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="itemId"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={item.id ?? ""}
|
||||||
|
readOnly
|
||||||
|
placeholder={item.id ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Name:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="itemName"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={item.testName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Status:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<StatusDropDown statusCode={item.statusCode}></StatusDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Value:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="numValue"
|
||||||
|
className="p-2 rounded"
|
||||||
|
defaultValue={item.numValue}
|
||||||
|
placeholder="Enter a numeric value"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Created At:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(item.createdAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemData = () => {
|
||||||
|
const id = pathName.split("/");
|
||||||
|
const idValue = id[id.length - 1];
|
||||||
|
isUpdateMode = idValue != "create";
|
||||||
|
if (isUpdateMode) {
|
||||||
|
const { item, isLoading, error } = useSampleItem(idValue as UUID);
|
||||||
|
if (!isLoading && !error && item) {
|
||||||
|
return renderForm(item);
|
||||||
|
} else return <>Loading</>;
|
||||||
|
} else {
|
||||||
|
return renderForm({
|
||||||
|
id: "0000-0000-0000-0000-0000",
|
||||||
|
testName: "",
|
||||||
|
statusCode: "P",
|
||||||
|
createdAt: new Date(),
|
||||||
|
numValue: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div id="formContainer"
|
||||||
|
className="flex flex-col">
|
||||||
|
<div>
|
||||||
|
<ItemData></ItemData>
|
||||||
|
<div className="flex flex-row justify-center pb-10">
|
||||||
|
<a
|
||||||
|
onClick={handleSaveClick}
|
||||||
|
className="button saveBtn">
|
||||||
|
Save
|
||||||
|
</a>
|
||||||
|
<Link className="button cancelBtn" href="../sample">
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/app/sample/page.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { deleteSampleItem, useSampleItems } from "@/hooks/use-sample-items";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from "next/link";
|
||||||
|
import Loader from '../../components/ui/loader.svg';
|
||||||
|
|
||||||
|
export default function Sample() {
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
Intl.DateTimeFormat("en-US").format(new Date(date));
|
||||||
|
let refreshData: any;
|
||||||
|
|
||||||
|
const DeleteButton = (itemId: any) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="button deleteBtn"
|
||||||
|
value="Delete"
|
||||||
|
onClick={async () => {await deleteSampleItem(itemId.itemId); refreshData()}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemsData = () => {
|
||||||
|
const { items, error, isLoading, mutate } = useSampleItems();
|
||||||
|
refreshData = mutate;
|
||||||
|
if (!isLoading && !error && items != null) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<Link href={`sample/${item.id}`}>{item.testName}</Link>
|
||||||
|
</td>
|
||||||
|
<td>{item.statusCode}</td>
|
||||||
|
<td>{item.numValue}</td>
|
||||||
|
<td>{formatDate(item.createdAt)}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex flex-nowrap">
|
||||||
|
<Link className="button editBtn" href={`sample/${item.id}`}>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<DeleteButton itemId={item.id}></DeleteButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
} else return Loading();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = () => (
|
||||||
|
<tr>
|
||||||
|
<td className="flex align-items-end" colSpan={5}><Image src={Loader} alt='Loading' width={62} />Loading</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div id="container" className="flex align-items-start place-content-center">
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={5}>
|
||||||
|
<Link className="button" href={`sample/create`}>
|
||||||
|
<span>Add new</span>
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Suspense fallback={Loading()}>
|
||||||
|
<ItemsData></ItemsData>
|
||||||
|
</Suspense>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{process.env.PUBLIC_API_URL}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
331
src/app/user/[id]/page.tsx
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createUser, updateUser, useUserItem } from "@/hooks/user";
|
||||||
|
import { User } from "@/lib/interfaces";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
|
||||||
|
export default function UserDetails() {
|
||||||
|
const [item, setItem] = useState<User>();
|
||||||
|
const pathName = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
let isUpdateMode = false;
|
||||||
|
|
||||||
|
const formatDate = (date: Date) =>
|
||||||
|
new Intl.DateTimeFormat("en-US").format(date);
|
||||||
|
|
||||||
|
function formatDateTime(date: Date) {
|
||||||
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusDropDown = (data: any) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
className="p-3 rounded"
|
||||||
|
defaultValue={data.statusCode}
|
||||||
|
>
|
||||||
|
<option value="Active">{Status.Active}</option>
|
||||||
|
<option value="Inactive">{Status.Inactive}</option>
|
||||||
|
<option value="Deleted">{Status.Deleted}</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
let id = (document.getElementById("id") as HTMLInputElement)?.value;
|
||||||
|
let email = (document.getElementById("email") as HTMLInputElement)?.value;
|
||||||
|
let name = (document.getElementById("name") as HTMLInputElement)?.value;
|
||||||
|
let middleName = (document.getElementById("middleName") as HTMLInputElement)
|
||||||
|
?.value;
|
||||||
|
let lastName = (document.getElementById("lastName") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
let roleId = (document.getElementById("roleId") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
let comp = (document.getElementById("companies") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
let proj = (document.getElementById("projects") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
let status = (document.getElementById("status") as HTMLSelectElement)
|
||||||
|
?.value;
|
||||||
|
let companies: string[] = comp ? comp.split(",") : [];
|
||||||
|
let projects: string[] = proj ? proj.split(",") : [];
|
||||||
|
|
||||||
|
let updatedUser = {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
middleName,
|
||||||
|
lastName,
|
||||||
|
roleId,
|
||||||
|
companies,
|
||||||
|
projects,
|
||||||
|
status,
|
||||||
|
} as unknown as User;
|
||||||
|
if (isUpdateMode) {
|
||||||
|
updateUser(updatedUser);
|
||||||
|
} else {
|
||||||
|
updatedUser.id = null;
|
||||||
|
createUser(updatedUser);
|
||||||
|
}
|
||||||
|
setItem(updatedUser);
|
||||||
|
router.push("/user");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = (user: User) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Id:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="id"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.id ?? ""}
|
||||||
|
readOnly
|
||||||
|
placeholder={user.id ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>GUID:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="guid"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.guid ?? ""}
|
||||||
|
readOnly
|
||||||
|
placeholder={user.guid ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Email</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.email ?? ""}
|
||||||
|
placeholder={user.email ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Name:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>MiddleName:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="middleName"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.middleName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>LastName:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="lastName"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.lastName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Role:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="roleId"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.roleId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Companies:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="companies"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.companies}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Projects:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="projects"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={user.projects}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Last login:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="lastLogin"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={
|
||||||
|
user.lastLogIn ? formatDateTime(new Date(user.lastLogIn)) : ""
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
placeholder={
|
||||||
|
user.lastLogIn ? formatDateTime(new Date(user.lastLogIn)) : ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Last logout:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="lastLogout"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
defaultValue={
|
||||||
|
user.lastLogOut ? formatDateTime(new Date(user.lastLogOut)) : ""
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
placeholder={
|
||||||
|
user.lastLogOut ? formatDateTime(new Date(user.lastLogOut)) : ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(user.createdAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>CreatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="createdBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={user.createdBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedAt:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedAt"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={formatDate(new Date(user.updatedAt))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>UpdatedBy:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<input
|
||||||
|
id="updatedBy"
|
||||||
|
className="p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
defaultValue={user.updatedBy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-5">
|
||||||
|
<label>Status:</label>
|
||||||
|
<div className="flex pt-2 flex-col">
|
||||||
|
<StatusDropDown statusCode={user.status}></StatusDropDown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemData = () => {
|
||||||
|
const id = pathName.split("/");
|
||||||
|
const idValue = id[id.length - 1];
|
||||||
|
isUpdateMode = idValue != "create";
|
||||||
|
if (isUpdateMode) {
|
||||||
|
const { item, isLoading, error } = useUserItem(idValue);
|
||||||
|
if (!isLoading && !error && item) {
|
||||||
|
return renderForm(item);
|
||||||
|
} else return <>Loading</>;
|
||||||
|
} else {
|
||||||
|
return renderForm({
|
||||||
|
id: "",
|
||||||
|
guid: "",
|
||||||
|
email: "",
|
||||||
|
name: "",
|
||||||
|
middleName: "",
|
||||||
|
lastName: "",
|
||||||
|
displayName: "",
|
||||||
|
roleId: "",
|
||||||
|
companies: [],
|
||||||
|
projects: [],
|
||||||
|
status: Status.Active,
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdBy: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: "",
|
||||||
|
lastLogIn: new Date(),
|
||||||
|
lastLogOut: new Date(),
|
||||||
|
token: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="formContainer" className="flex flex-col">
|
||||||
|
<div>
|
||||||
|
<ItemData></ItemData>
|
||||||
|
<div className="flex flex-row justify-center pb-10">
|
||||||
|
<a onClick={handleSaveClick} className="button saveBtn">
|
||||||
|
Save
|
||||||
|
</a>
|
||||||
|
<Link className="button cancelBtn" href="../user">
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
src/app/user/page.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useUserItems, ChangeUserStatus } from "@/hooks/user";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Loader from "../../components/ui/loader.svg";
|
||||||
|
import { ChangeStatusRequest, Role } from "@/lib/interfaces";
|
||||||
|
import { Status } from "@/lib/Enums";
|
||||||
|
import { useRoleItems } from "@/hooks/role";
|
||||||
|
|
||||||
|
export default function User() {
|
||||||
|
let refreshData: any;
|
||||||
|
|
||||||
|
const DeleteButton = (itemId: any) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="button deleteBtn"
|
||||||
|
value="Delete"
|
||||||
|
onClick={async () => {
|
||||||
|
const statusRequest: ChangeStatusRequest = {
|
||||||
|
id: itemId.itemId,
|
||||||
|
status: Status.Deleted,
|
||||||
|
};
|
||||||
|
await ChangeUserStatus(statusRequest);
|
||||||
|
refreshData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemsData = () => {
|
||||||
|
const { items, error, isLoading, mutate } = useUserItems();
|
||||||
|
const {
|
||||||
|
items: roleItems,
|
||||||
|
error: roleError,
|
||||||
|
isLoading: isRoleLoading,
|
||||||
|
mutate: mutateRole,
|
||||||
|
} = useRoleItems();
|
||||||
|
const roleLookup = roleItems?.reduce((acc: any, role: Role) => {
|
||||||
|
if (role && role.id != null && role.name) {
|
||||||
|
acc[role.id] = role.name;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
refreshData = mutate;
|
||||||
|
if (!isLoading && !error && items != null) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<Link href={`user/${item.id}`}>{item.displayName}</Link>
|
||||||
|
</td>
|
||||||
|
<td>{item.email}</td>
|
||||||
|
<td>{roleLookup[item.roleId] || [item.roleId]}</td>{" "}
|
||||||
|
<td>{item.status}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex flex-nowrap">
|
||||||
|
<Link className="button editBtn" href={`user/${item.id}`}>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<DeleteButton itemId={item.id}></DeleteButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
} else return Loading();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = () => (
|
||||||
|
<tr>
|
||||||
|
<td className="flex align-items-end" colSpan={5}>
|
||||||
|
<Image src={Loader} alt="Loading" width={62} />
|
||||||
|
Loading
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div id="container" className="flex align-items-start place-content-center">
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={5}>
|
||||||
|
<Link className="button" href={`user/create`}>
|
||||||
|
<span>Add new</span>
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Suspense fallback={Loading()}>
|
||||||
|
<ItemsData></ItemsData>
|
||||||
|
</Suspense>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{process.env.PUBLIC_CERBEROS_API_URL}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/footer/index.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'use client'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import UpstartLogo from '../ui/upstartLogo.svg'
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
<small>
|
||||||
|
Blueprint is a proud product of
|
||||||
|
</small>
|
||||||
|
<Image src={UpstartLogo} alt='Upstart13 Brand' width={120} />
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
||||||
128
src/components/header/index.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
'use client'
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import ProfileAvatar from "@images/avatar.svg";
|
||||||
|
import { BellIcon, ChevronDownIcon, RefreshIcon } from "../ui/icons";
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
Transition,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import msalInstance from "@/lib/msalInstance";
|
||||||
|
import { deleteAllCookies } from "@/lib/AuthCookie";
|
||||||
|
import { logoutUser } from "@/hooks/user";
|
||||||
|
import useUserFromCookie from "@/lib/useUser";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const pathName = usePathname();
|
||||||
|
|
||||||
|
const { userData, setUser } = useUserFromCookie({
|
||||||
|
redirectTo: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
const userNavigation = [
|
||||||
|
{ name: "Your profile", href: "#" },
|
||||||
|
{ name: "Sign out", href: "#" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function classNames(...classes: any[]) {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
logoutUser(userData?.email ?? "");
|
||||||
|
deleteAllCookies();
|
||||||
|
setUser(null);
|
||||||
|
msalInstance.logoutPopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<div className="flex self-stretch flex-1 justify-between py-3 lg:px-9">
|
||||||
|
<div className="flex">
|
||||||
|
<h1>
|
||||||
|
<Link href="/"><blue("print")></Link>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h3 className="capitalize text-primary-800">
|
||||||
|
{pathName?.split("/")[1]}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{userData ? (
|
||||||
|
<div id="headerTools" className="flex items-center gap-2">
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<RefreshIcon />
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<BellIcon />
|
||||||
|
</div>
|
||||||
|
<Image
|
||||||
|
src={ProfileAvatar}
|
||||||
|
className="hidden sm:block"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
alt="profile Avatar"
|
||||||
|
/>
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<MenuButton className="-m-1.5 flex items-center p-1.5 gap-2">
|
||||||
|
<span className="hidden lg:flex lg:items-center">
|
||||||
|
<span className="sr-only">Open user menu</span>
|
||||||
|
<span className="text-primary-800 font-normal">
|
||||||
|
{userData?.name}
|
||||||
|
<br></br>
|
||||||
|
{userData?.email}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="ml-2 h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</MenuButton>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<MenuItems className="absolute right-0 z-10 mt-2.5 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||||
|
{userNavigation.map((item) => (
|
||||||
|
<MenuItem key={item.name}>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (item.name === "Sign out") {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-50" : "",
|
||||||
|
"block px-3 py-1 text-sm leading-6 text-gray-900"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuItems>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header
|
||||||
36
src/components/headerImage/index.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSampleImage } from "@/hooks/use-sample-images";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const HeaderImage = () => {
|
||||||
|
const [style, setStyle] = useState<{
|
||||||
|
backgroundImage: string;
|
||||||
|
marginTop: number;
|
||||||
|
}>();
|
||||||
|
useEffect(() => {
|
||||||
|
const getStyleData = async () => {
|
||||||
|
var result = await useSampleImage();
|
||||||
|
setStyle({
|
||||||
|
backgroundImage: `url('${result.url}')`,
|
||||||
|
marginTop: -10,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!style) {
|
||||||
|
getStyleData();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={style}>
|
||||||
|
<button id="ImageLoadButton" className="top-0 p-2 rounded">
|
||||||
|
Change Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderImage;
|
||||||
57
src/components/side-nav/side-nav.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
import useUserFromCookie from "@/lib/useUser";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const SideNav = () => {
|
||||||
|
const { userData } = useUserFromCookie({
|
||||||
|
redirectTo: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [openMenuBool, setOpenMenuBool] = useState(false);
|
||||||
|
|
||||||
|
function ToggleMenu() {
|
||||||
|
setOpenMenuBool((prev) => !prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="mainMenu">
|
||||||
|
{userData ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={`menuBtn ${
|
||||||
|
openMenuBool ? "IconCloseMenu" : "IconOpenMenu"
|
||||||
|
}`}
|
||||||
|
onClick={ToggleMenu}
|
||||||
|
>
|
||||||
|
<span>Menu</span>
|
||||||
|
</button>
|
||||||
|
{openMenuBool && (
|
||||||
|
<nav className="menuContainer">
|
||||||
|
<ul>
|
||||||
|
{userData.modules && userData.modules.length > 0 ? (
|
||||||
|
userData.modules.map(
|
||||||
|
(
|
||||||
|
module: { name: string; route: string },
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<li key={index}>
|
||||||
|
<Link href={module.route}>{module.name}</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<li>No modules available</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideNav;
|
||||||
41
src/components/ui/forms/input.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface InputProps {
|
||||||
|
label: string;
|
||||||
|
inputStyle: 'normal' | 'Floating filled';
|
||||||
|
value?: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = ({ label, inputStyle, value, icon }: InputProps) => {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{inputStyle === 'normal' ? (
|
||||||
|
<div className="py-2 px-3 border rounded-md">
|
||||||
|
<input className="w-full focus:outline-none" type="text" placeholder={label} />
|
||||||
|
{icon && <div className="absolute right-3 top-1/2 transform -translate-y-1/2">{icon}</div>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
type="text"
|
||||||
|
id={`floating_filled_${label}`}
|
||||||
|
className="block rounded-t-lg px-2.5 pb-2.5 pt-6 w-full text-sm text-gray-900 bg-gray-50 dark:bg-gray-700 border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
|
||||||
|
placeholder=" "
|
||||||
|
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`floating_filled_${label}`}
|
||||||
|
className="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] start-2.5 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{icon && <div className="absolute right-3 top-1/2 transform -translate-y-1/3">{icon}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
||||||
27
src/components/ui/icons/bell-icon.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { SVGProps } from 'react'
|
||||||
|
|
||||||
|
function BellIcon({ ...props }: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36" { ...props }>
|
||||||
|
<g clipPath="url(#clip0_25_2117)">
|
||||||
|
<path fill="#fff" fillOpacity="0.01" d="M0 0H20V20H0z" transform="translate(8 8)"></path>
|
||||||
|
<g clipPath="url(#clip1_25_2117)">
|
||||||
|
<path
|
||||||
|
fill="#FFF"
|
||||||
|
d="M18 28a2.5 2.5 0 002.5-2.5h-5A2.5 2.5 0 0018 28zm0-17.602l-.996.2A5.003 5.003 0 0013 15.5c0 .785-.168 2.746-.574 4.677-.2.96-.47 1.958-.829 2.823h12.806c-.36-.865-.628-1.863-.83-2.823-.405-1.93-.573-3.892-.573-4.677a5.002 5.002 0 00-4.004-4.9L18 10.396v.002zM25.775 23c.279.559.601 1.001.975 1.25H9.25c.374-.249.696-.691.975-1.25 1.125-2.25 1.525-6.4 1.525-7.5 0-3.025 2.15-5.55 5.006-6.126a1.25 1.25 0 112.488 0A6.252 6.252 0 0124.25 15.5c0 1.1.4 5.25 1.525 7.5z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_25_2117">
|
||||||
|
<path fill="#fff" d="M0 0H20V20H0z" transform="translate(8 8)"></path>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_25_2117">
|
||||||
|
<path fill="#fff" d="M0 0H20V20H0z" transform="translate(8 8)"></path>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BellIcon
|
||||||
13
src/components/ui/icons/chevron-down-icon.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SVGProps } from 'react'
|
||||||
|
|
||||||
|
function ChevronDownIcon({ ...props }: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20" {...props}>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFF" fillRule="evenodd" d="M1.941 8.47a.625.625 0 01.838-.279L10 11.8l7.22-3.61a.625.625 0 11.56 1.117l-7.5 3.75a.624.624 0 01-.56 0l-7.5-3.75a.625.625 0 01-.279-.837z" clipRule="evenodd"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChevronDownIcon
|
||||||
3
src/components/ui/icons/close-menu-icon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 18H16V16H3V18ZM3 13H13V11H3V13ZM3 6V8H16V6H3ZM21 15.59L17.42 12L21 8.41L19.59 7L14.59 12L19.59 17L21 15.59Z" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 241 B |
13
src/components/ui/icons/dashboard-icon.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SVGProps } from 'react'
|
||||||
|
|
||||||
|
function DashboardIcon({ ...props }: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFF" d="M19 5v2h-4V5h4zM9 5v6H5V5h4zm10 8v6h-4v-6h4zM9 17v2H5v-2h4zM21 3h-8v6h8V3zM11 3H3v10h8V3zm10 8h-8v10h8V11zm-10 4H3v6h8v-6z"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardIcon
|
||||||
11
src/components/ui/icons/index.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import DashboardIcon from "./dashboard-icon";
|
||||||
|
import BellIcon from "./bell-icon";
|
||||||
|
import ChevronDownIcon from "./chevron-down-icon";
|
||||||
|
import RefreshIcon from "./refresh-icon";
|
||||||
|
|
||||||
|
export {
|
||||||
|
DashboardIcon,
|
||||||
|
BellIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
RefreshIcon
|
||||||
|
}
|
||||||
3
src/components/ui/icons/open-menu-icon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 18H21V16H3V18ZM3 13H21V11H3V13ZM3 6V8H21V6H3Z" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 179 B |
20
src/components/ui/icons/refresh-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { SVGProps } from 'react'
|
||||||
|
|
||||||
|
function RefreshIcon({ ...props }: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36" {...props}>
|
||||||
|
<path
|
||||||
|
fill="#FFF"
|
||||||
|
d="M22.418 16.75h4.915a.313.313 0 01.24.512l-2.458 2.95a.311.311 0 01-.48 0l-2.458-2.95a.312.312 0 01.24-.512zm-13.75 2.5h4.915a.312.312 0 00.24-.512l-2.458-2.95a.312.312 0 00-.48 0l-2.458 2.95a.313.313 0 00.24.512z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#FFF"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M18 11.75c-1.94 0-3.675.884-4.821 2.273a.626.626 0 11-.964-.796 7.502 7.502 0 0113.181 3.523h-1.271a6.253 6.253 0 00-6.125-5zm-6.125 7.5a6.252 6.252 0 0010.946 2.727.622.622 0 01.673-.217.624.624 0 01.291 1.012 7.502 7.502 0 01-13.181-3.522h1.271z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RefreshIcon
|
||||||
95
src/components/ui/layout/app-layout.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
'use client'
|
||||||
|
import { Fragment, useState } from 'react'
|
||||||
|
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'
|
||||||
|
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||||
|
import Header from '@/components/header'
|
||||||
|
import SideMenu from "@/components/side-nav/side-nav";
|
||||||
|
import SideNav from "./side-nav";
|
||||||
|
|
||||||
|
export default function AppLayout() {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile Navigation */}
|
||||||
|
<Transition show={sidebarOpen} as={Fragment}>
|
||||||
|
<Dialog className="relative z-50 lg:hidden" onClose={setSidebarOpen}>
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition-opacity ease-linear duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="transition-opacity ease-linear duration-300"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-gray-900/80" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 flex">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-in-out duration-300 transform"
|
||||||
|
enterFrom="-translate-x-full"
|
||||||
|
enterTo="translate-x-0"
|
||||||
|
leave="transition ease-in-out duration-300 transform"
|
||||||
|
leaveFrom="translate-x-0"
|
||||||
|
leaveTo="-translate-x-full"
|
||||||
|
>
|
||||||
|
<DialogPanel className="relative mr-16 flex w-full max-w-64 flex-1">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-in-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in-out duration-300"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="-m-2.5 p-2.5"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close sidebar</span>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
<div className="flex grow flex-col overflow-y-auto bg-white max-w-64">
|
||||||
|
<SideNav
|
||||||
|
handleNavItemOnClick={() => setSidebarOpen((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden lg:fixed lg:inset-y-0 lg:z-30 lg:flex lg:w-64 lg:flex-col">
|
||||||
|
<SideNav handleNavItemOnClick={() => setSidebarOpen((prev) => !prev)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header component */}
|
||||||
|
<div className="lg:pl-64 fixed w-full z-20">
|
||||||
|
<div className="sticky top-0 z-20 flex h-16 shrink-0 items-center gap-x-4 bg-white sm:gap-x-6 lg:gap-x-0 px-4 lg:px-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open sidebar</span>
|
||||||
|
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<Header />
|
||||||
|
<SideMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/ui/layout/nav-links.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { DashboardIcon } from '../icons'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { ILink } from '@/lib/interfaces'
|
||||||
|
|
||||||
|
const links: ILink[] = [
|
||||||
|
{
|
||||||
|
name: 'Form Sample',
|
||||||
|
href: '/formSample',
|
||||||
|
icon: <DashboardIcon />,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
interface NavLinksProps {
|
||||||
|
handleNavItemOnClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavLinks({ handleNavItemOnClick }: NavLinksProps) {
|
||||||
|
const pathName = usePathname()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav>
|
||||||
|
<ul role="list" className="space-y-2">
|
||||||
|
{links
|
||||||
|
.filter((link) => !link.hide)
|
||||||
|
.map((link) => {
|
||||||
|
return (
|
||||||
|
<li key={link.name} onClick={handleNavItemOnClick}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className={`flex h-[48px] text-primary-main grow items-center gap-8 ${link.href === pathName ? 'bg-primary-100' : 'bg-white'}
|
||||||
|
p-3 text-base font-medium hover:bg-primary-100 flex-none justify-start`}
|
||||||
|
>
|
||||||
|
{link.icon}
|
||||||
|
<p>{link.name}</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
32
src/components/ui/layout/side-nav.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import heathUsLogo from '@images/heath-logo-blue.png'
|
||||||
|
import Input from '../forms/input'
|
||||||
|
import NavLinks from './nav-links'
|
||||||
|
|
||||||
|
interface SideNavProps {
|
||||||
|
handleNavItemOnClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SideNav = ({ handleNavItemOnClick }: SideNavProps ) => {
|
||||||
|
return (
|
||||||
|
<div className="flex grow h-full flex-col overflow-y-auto px-3 py-5 shadow-sideBar bg-white">
|
||||||
|
<Link className="mb-2 flex items-end justify-start rounded-md bg-white px-3" href="/">
|
||||||
|
<div className="text-white flex shrink-0 items-center">
|
||||||
|
<Image className="h-8 w-auto" src={heathUsLogo} alt="Heath Us Logo" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div className="flex grow justify-between flex-col md:space-x-0 space-y-2 pt-4">
|
||||||
|
<div className="px-4">
|
||||||
|
<Input label="Filter options" inputStyle="normal" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-2 pt-4">
|
||||||
|
<NavLinks handleNavItemOnClick={handleNavItemOnClick}/>
|
||||||
|
</div>
|
||||||
|
<div className="h-auto w-full grow block"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SideNav
|
||||||
116
src/components/ui/loader.svg
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" width="62" height="20" viewBox="0 0 62 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
|
||||||
|
<style rel="text/CSS">
|
||||||
|
.flecha{
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path id="load01" class="flecha" d="M2 2L11 10L2 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load02" class="flecha" d="M9 2L18 10L9 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load03" class="flecha" d="M16 2L25 10L16 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load04" class="flecha" d="M23 2L32 10L23 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load05" class="flecha" d="M30 2L39 10L30 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load06" class="flecha" d="M37 2L46 10L37 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load07" class="flecha" d="M44 2L53 10L44 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="load08" class="flecha" d="M51 2L60 10L51 18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<animate
|
||||||
|
xlink:href="#load01"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="0s; Load08.end + 0.5"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load01"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<animate
|
||||||
|
xlink:href="#load02"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load01.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load02"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<animate
|
||||||
|
xlink:href="#load03"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load02.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load03"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
xlink:href="#load04"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load03.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load04"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
xlink:href="#load05"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load04.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load05"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
xlink:href="#load06"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load05.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load06"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
xlink:href="#load07"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load06.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load07"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
xlink:href="#load08"
|
||||||
|
attributeName="opacity"
|
||||||
|
attributeType="CSS"
|
||||||
|
from="0"
|
||||||
|
to="1"
|
||||||
|
begin="Load07.end"
|
||||||
|
dur="0.15s"
|
||||||
|
fill="freeze"
|
||||||
|
restart="always"
|
||||||
|
id="Load08"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
12
src/components/ui/upstartLogo.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg width="326" height="67" viewBox="0 0 326 67" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M312.969 31.2697L324.374 18.8775V11.4368H292.807V20.1932H311.84L299.998 33.2433V39.2794L300.363 39.1549C301.581 38.7371 303.901 38.4526 305.208 38.4526C312.533 38.4526 316.569 41.3684 316.569 46.6489C316.569 51.5383 313.084 54.7031 307.679 54.7031C302.274 54.7031 297.092 52.5073 292.922 48.6669L292.46 48.2491V58.2144L292.567 58.3034C294.994 60.2324 300.203 63.4594 307.75 63.4594C320.32 63.4594 326.001 56.6766 326.001 46.5778C326.001 38.3637 320.907 32.4165 312.969 31.2786V31.2697Z" fill="#FFD900"/>
|
||||||
|
<path d="M172.218 43.2264C165.764 43.2264 160.866 38.1504 160.866 31.2609C160.866 24.3713 165.764 19.2864 172.218 19.2864C178.672 19.2864 183.57 24.3624 183.57 31.2609C183.57 38.1593 178.672 43.2264 172.218 43.2264ZM172.067 10.2456C159.861 10.2456 150.954 19.2865 150.954 31.252C150.954 43.2176 159.47 52.2584 170.209 52.2584C176.352 52.2584 181.17 49.4048 183.588 44.4888V51.3072H193.313L193.358 31.252C193.358 19.1976 183.988 10.2456 172.084 10.2456H172.067Z" fill="#FFD900"/>
|
||||||
|
<path d="M236.793 0.235596H227.067V35.6789C227.067 46.9333 233.521 52.4093 242.153 52.4093C244.642 52.4093 247.594 51.8493 250.394 50.587V41.8661C248.527 42.9774 246.509 43.6885 243.86 43.6885C240.046 43.6885 236.784 41.0749 236.784 35.1277V19.6596H250.394V11.3388H236.784V0.235596H236.793Z" fill="#FFD900"/>
|
||||||
|
<path d="M64.4656 42.6753C58.4028 42.6753 53.6557 38.1593 53.6557 31.2609C53.6557 24.3624 58.4028 19.7665 64.4656 19.7665C70.5283 19.7665 75.2755 24.3624 75.2755 31.2609C75.2755 38.1593 70.6084 42.6753 64.4656 42.6753ZM64.5456 10.2456C52.6422 10.2456 43.228 19.2064 43.228 31.252V67.0064H52.9534V48.6936C56.2159 50.9161 60.2696 52.2584 64.5456 52.2584C76.7511 52.2584 85.7031 43.1464 85.7031 31.252C85.7031 19.3576 76.76 10.2456 64.5456 10.2456Z" fill="#FFD900"/>
|
||||||
|
<path d="M109.145 27.5272L104.087 26.336C100.975 25.5448 99.891 24.4336 99.891 22.6912C99.891 20.5488 101.838 19.2064 105.1 19.2064C109.999 19.2064 115.288 20.8688 118.479 23.4823V13.8104C115.368 11.668 110.47 10.2456 105.491 10.2456C97.5529 10.2456 90.1656 13.5792 90.1656 22.9312C90.1656 28.0872 92.9659 32.6032 101.518 34.5856L106.185 35.6969C109.296 36.488 110.461 37.6792 110.461 39.6616C110.461 41.9552 108.594 43.4665 105.091 43.4665C99.9532 43.4665 93.9704 41.244 89.7656 37.7592L89.6855 47.5113C94.1126 50.4449 99.482 52.2673 104.149 52.2673C114.568 52.2673 120.328 47.6713 120.328 39.8216C120.328 33.9544 117.448 29.4385 109.127 27.5361L109.145 27.5272Z" fill="#FFD900"/>
|
||||||
|
<path d="M134.312 0.102295H124.587V35.5456C124.587 46.8 131.041 52.276 139.673 52.276C142.162 52.276 145.113 51.7249 147.914 50.4537V41.7328C146.047 42.8441 144.029 43.5552 141.38 43.5552C137.566 43.5552 134.303 40.9416 134.303 34.9944V19.5264H147.914V11.2055H134.303V0.102295H134.312Z" fill="#FFD900"/>
|
||||||
|
<path d="M200.025 29.0387V51.4496H209.741V30.541C209.741 23.6515 212.231 19.6867 217.351 19.6867C218.676 19.6867 221.467 20.2379 222.24 20.7979V11.3748C220.845 10.8236 217.422 10.4236 215.635 10.4236C204.603 10.4236 200.016 17.5531 200.016 29.0387H200.025Z" fill="#FFD900"/>
|
||||||
|
<path d="M28.7022 22.1221C28.7022 28.2205 25.1286 32.1053 19.688 32.1053C14.2475 32.1053 10.6739 28.2205 10.6739 22.1221V0.102295H0.966309V22.4333C0.966309 33.7588 8.34477 41.1195 19.6969 41.1195C31.0491 41.1195 38.4276 33.7499 38.4276 22.4333V0.102295H28.72V22.1221H28.7022Z" fill="#FFD900"/>
|
||||||
|
<path d="M19.6877 44.9333C13.2516 44.9333 8.09553 47.3158 4.83301 51.4406H34.5424C31.2799 47.3158 26.1328 44.9333 19.6877 44.9333Z" fill="#FFD900"/>
|
||||||
|
<path d="M265.427 21.2956V31.1543C269.143 28.8785 273.054 26.0338 276.592 22.9046V51.4495H285.393V11.3391H279.481C275.01 15.0372 270.174 18.5487 265.436 21.2956H265.427Z" fill="#FFD900"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
88
src/hooks/module.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { ChangeStatusRequest, Module } from "@/lib/interfaces";
|
||||||
|
import {
|
||||||
|
PatchData,
|
||||||
|
deleteData,
|
||||||
|
getData,
|
||||||
|
postData,
|
||||||
|
putData,
|
||||||
|
} from "@/services/cerberos-api";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetchData = async (url: string): Promise<{ result: Module[] }> => {
|
||||||
|
const response = await getData<{ result: Module[] }>(url);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the permission data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOne = async (
|
||||||
|
url: string,
|
||||||
|
id: string
|
||||||
|
): Promise<{ result: Module }> => {
|
||||||
|
const data = {
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
const response = await postData<{ result: Module }>(url, data);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the permission data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useModuleItems = () => {
|
||||||
|
const url = "/Module/GetAll";
|
||||||
|
const fetcher = useCallback(() => fetchData(url), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{
|
||||||
|
result: Module[];
|
||||||
|
}>(url, fetcher);
|
||||||
|
var items = !isLoading ? (data as unknown as Module[]) : ([] as Module[]);
|
||||||
|
|
||||||
|
return { items, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useModuleItem = (id: string) => {
|
||||||
|
const url = "/Module/GetById";
|
||||||
|
const fetcher = useCallback(() => fetchOne(url, id), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{
|
||||||
|
result: Module;
|
||||||
|
}>(`${url}${id}`, fetcher);
|
||||||
|
let item = !isLoading ? (data as unknown as Module) : ({} as Module);
|
||||||
|
return { item, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateModule = async (request: Module): Promise<Module> => {
|
||||||
|
const url = `/Module/Update`;
|
||||||
|
const response = await putData(url, request);
|
||||||
|
return response as Module;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createModule = async (item: Module): Promise<Module> => {
|
||||||
|
const url = `/Module/Create`;
|
||||||
|
const response = await postData(url, item);
|
||||||
|
return response as Module;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteModule = async (id: UUID): Promise<void> => {
|
||||||
|
const url = `/Module/${id}`;
|
||||||
|
await deleteData(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeModuleStatus = async (
|
||||||
|
newStatus: ChangeStatusRequest
|
||||||
|
): Promise<Module> => {
|
||||||
|
const url = `/Module/ChangeStatus`;
|
||||||
|
const response = await PatchData<Module>(url, newStatus);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useModuleItems,
|
||||||
|
useModuleItem,
|
||||||
|
updateModule,
|
||||||
|
createModule,
|
||||||
|
deleteModule,
|
||||||
|
ChangeModuleStatus,
|
||||||
|
};
|
||||||
100
src/hooks/permission.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { ChangeStatusRequest, Permission } from "@/lib/interfaces";
|
||||||
|
import {
|
||||||
|
PatchData,
|
||||||
|
deleteData,
|
||||||
|
getData,
|
||||||
|
postData,
|
||||||
|
putData,
|
||||||
|
} from "@/services/cerberos-api";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetchData = async (url: string): Promise<{ result: Permission[] }> => {
|
||||||
|
const response = await getData<{ result: Permission[] }>(url);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the permission data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOne = async (
|
||||||
|
url: string,
|
||||||
|
id: string
|
||||||
|
): Promise<{ result: Permission }> => {
|
||||||
|
const data = {
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
const response = await postData<{ result: Permission }>(url, data);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the permission data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePermissionItems = () => {
|
||||||
|
const url = "/Permission/GetAll";
|
||||||
|
const fetcher = useCallback(() => fetchData(url), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{
|
||||||
|
result: Permission[];
|
||||||
|
}>(url, fetcher);
|
||||||
|
var items = !isLoading
|
||||||
|
? (data as unknown as Permission[])
|
||||||
|
: ([] as Permission[]);
|
||||||
|
|
||||||
|
return { items, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePermissionItem = (id: string) => {
|
||||||
|
const url = "/Permission/GetById";
|
||||||
|
const fetcher = useCallback(() => fetchOne(url, id), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{
|
||||||
|
result: Permission;
|
||||||
|
}>(`${url}${id}`, fetcher);
|
||||||
|
let item = !isLoading ? (data as unknown as Permission) : ({} as Permission);
|
||||||
|
return { item, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePermission = async (request: Permission): Promise<Permission> => {
|
||||||
|
const url = `/Permission/Update`;
|
||||||
|
const response = await putData(url, request);
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPermission = async (item: Permission): Promise<Permission> => {
|
||||||
|
const url = `/Permission/Create`;
|
||||||
|
const response = await postData(url, item);
|
||||||
|
return response as Permission;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionByEmail = async (email: string): Promise<Permission> => {
|
||||||
|
const url = `/Permission/GetByEmail`;
|
||||||
|
const request = {
|
||||||
|
email: email,
|
||||||
|
};
|
||||||
|
const response = await postData(url, request);
|
||||||
|
return response as Permission;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePermission = async (id: UUID): Promise<void> => {
|
||||||
|
const url = `/Permission/${id}`;
|
||||||
|
await deleteData(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangePermissionStatus = async (
|
||||||
|
newStatus: ChangeStatusRequest
|
||||||
|
): Promise<Permission> => {
|
||||||
|
const url = `/Permission/ChangeStatus`;
|
||||||
|
const response = await PatchData<Permission>(url, newStatus);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
usePermissionItems,
|
||||||
|
usePermissionItem,
|
||||||
|
updatePermission,
|
||||||
|
createPermission,
|
||||||
|
deletePermission,
|
||||||
|
ChangePermissionStatus,
|
||||||
|
getPermissionByEmail,
|
||||||
|
};
|
||||||
87
src/hooks/role.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { ChangeStatusRequest, Role } from "@/lib/interfaces";
|
||||||
|
import {
|
||||||
|
PatchData,
|
||||||
|
deleteData,
|
||||||
|
getData,
|
||||||
|
postData,
|
||||||
|
putData,
|
||||||
|
} from "@/services/cerberos-api";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetchData = async (url: string): Promise<{ result: Role[] }> => {
|
||||||
|
const response = await getData<{ result: Role[] }>(url);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the role data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOne = async (url: string, id: string): Promise<{ result: Role }> => {
|
||||||
|
const data = {
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
const response = await postData<{ result: Role }>(url, data);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the role data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRoleItems = () => {
|
||||||
|
const url = "/Role/GetAll";
|
||||||
|
const fetcher = useCallback(() => fetchData(url), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{ result: Role[] }>(
|
||||||
|
url,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
var items = !isLoading ? (data as unknown as Role[]) : ([] as Role[]);
|
||||||
|
|
||||||
|
return { items, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRoleItem = (id: string) => {
|
||||||
|
const url = "/Role/GetById";
|
||||||
|
const fetcher = useCallback(() => fetchOne(url, id), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{ result: Role }>(
|
||||||
|
`${url}${id}`,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
let item = !isLoading ? (data as unknown as Role) : ({} as Role);
|
||||||
|
return { item, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRole = async (request: Role): Promise<Role> => {
|
||||||
|
const url = `/Role/Update`;
|
||||||
|
const response = await putData(url, request);
|
||||||
|
return response as Role;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRole = async (item: Role): Promise<Role> => {
|
||||||
|
const url = `/Role/Create`;
|
||||||
|
const response = await postData(url, item);
|
||||||
|
return response as Role;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRole = async (id: UUID): Promise<void> => {
|
||||||
|
const url = `/Role/${id}`;
|
||||||
|
await deleteData(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeRoleStatus = async (
|
||||||
|
newStatus: ChangeStatusRequest
|
||||||
|
): Promise<Role> => {
|
||||||
|
const url = `/Role/ChangeStatus`;
|
||||||
|
const response = await PatchData<Role>(url, newStatus);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useRoleItems,
|
||||||
|
useRoleItem,
|
||||||
|
updateRole,
|
||||||
|
createRole,
|
||||||
|
deleteRole,
|
||||||
|
ChangeRoleStatus,
|
||||||
|
};
|
||||||
11
src/hooks/use-sample-images.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ImageUrl } from "@/lib/interfaces";
|
||||||
|
import { deleteData, getData, postData, putData } from "@/services/api";
|
||||||
|
|
||||||
|
export const useSampleImage = async () => {
|
||||||
|
const url = '/blobsampleimage';
|
||||||
|
const response = await getData<ImageUrl>(url);
|
||||||
|
if(!response) {
|
||||||
|
throw new Error("An error occured while fetching the sample image data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
71
src/hooks/use-sample-items.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { SampleItem } from "@/lib/interfaces";
|
||||||
|
import { deleteData, getData, postData, putData } from "@/services/api";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetchData = async (url: string): Promise<{ result: SampleItem[] }> => {
|
||||||
|
const response = await getData<{ result: SampleItem[] }>(url);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the sample items data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOne = async (url: string): Promise<{ result: SampleItem }> => {
|
||||||
|
const response = await getData<{ result: SampleItem }>(url);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the sample items data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSampleItems = () => {
|
||||||
|
const url = '/sqlSampleItems';
|
||||||
|
const fetcher = useCallback(() => fetchData(url), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{ result: SampleItem[] }>(
|
||||||
|
url,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
var items = !isLoading
|
||||||
|
? (data as unknown as SampleItem[])
|
||||||
|
: ([] as SampleItem[]);
|
||||||
|
|
||||||
|
return { items, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSampleItem = (id: UUID) => {
|
||||||
|
const url = `/sqlSampleItems/${id}`;
|
||||||
|
const fetcher = useCallback(() => fetchOne(url), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{ result: SampleItem }>(
|
||||||
|
url,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
let item = !isLoading ? (data as unknown as SampleItem) : ({} as SampleItem);
|
||||||
|
return { item, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSampleItem = async (item: SampleItem): Promise<SampleItem> => {
|
||||||
|
const url = `/sqlSampleItems/${item.id}`;
|
||||||
|
const response = await putData(url, item);
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSampleItem = async (item: SampleItem): Promise<SampleItem> => {
|
||||||
|
const url = `/sqlSampleItems`;
|
||||||
|
const response = await postData(url, item);
|
||||||
|
return response as SampleItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSampleItem = async (id: UUID): Promise<void> => {
|
||||||
|
const url = `/sqlSampleItems/${id}`;
|
||||||
|
await deleteData(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useSampleItems,
|
||||||
|
useSampleItem,
|
||||||
|
updateSampleItem,
|
||||||
|
createSampleItem,
|
||||||
|
deleteSampleItem,
|
||||||
|
};
|
||||||
128
src/hooks/user.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { ChangeStatusRequest, User, UserExistence } from "@/lib/interfaces";
|
||||||
|
import {
|
||||||
|
PatchData,
|
||||||
|
deleteData,
|
||||||
|
getData,
|
||||||
|
postData,
|
||||||
|
putData,
|
||||||
|
} from "@/services/cerberos-api";
|
||||||
|
import { AxiosRequestConfig } from "axios";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetchData = async (url: string): Promise<{ result: User[] }> => {
|
||||||
|
const response = await getData<{ result: User[] }>(url);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the user data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOne = async (url: string, id: string): Promise<{ result: User }> => {
|
||||||
|
const data = {
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
const response = await postData<{ result: User }>(url, data);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("An error occurred while fetching the user data");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useUserItems = () => {
|
||||||
|
const url = "/User/GetAll";
|
||||||
|
const fetcher = useCallback(() => fetchData(url), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{ result: User[] }>(
|
||||||
|
url,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
var items = !isLoading ? (data as unknown as User[]) : ([] as User[]);
|
||||||
|
|
||||||
|
return { items, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useUserItem = (id: string) => {
|
||||||
|
const url = "/User/GetById";
|
||||||
|
const fetcher = useCallback(() => fetchOne(url, id), [url]);
|
||||||
|
const { data, error, mutate, isLoading } = useSWR<{ result: User }>(
|
||||||
|
`${url}${id}`,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
let item = !isLoading ? (data as unknown as User) : ({} as User);
|
||||||
|
return { item, error, mutate, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = async (request: User): Promise<User> => {
|
||||||
|
const url = `/User/Update`;
|
||||||
|
const response = await putData(url, request);
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = async (item: User): Promise<User> => {
|
||||||
|
const url = `/User/Create`;
|
||||||
|
const response = await postData(url, item);
|
||||||
|
return response as User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserByEmail = async (email: string): Promise<User> => {
|
||||||
|
const url = `/User/GetByEmail`;
|
||||||
|
const request = {
|
||||||
|
email: email,
|
||||||
|
};
|
||||||
|
const response = await postData(url, request);
|
||||||
|
return response as User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (id: UUID): Promise<void> => {
|
||||||
|
const url = `/User/${id}`;
|
||||||
|
await deleteData(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeUserStatus = async (
|
||||||
|
newStatus: ChangeStatusRequest
|
||||||
|
): Promise<User> => {
|
||||||
|
const url = `/User/ChangeStatus`;
|
||||||
|
const response = await PatchData<User>(url, newStatus);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginUser = async (email: string): Promise<User> => {
|
||||||
|
const request = {
|
||||||
|
email: email,
|
||||||
|
};
|
||||||
|
const url = `/User/LoginUser`;
|
||||||
|
const response = await PatchData<User>(url, request);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutUser = async (email: string): Promise<User> => {
|
||||||
|
const request = {
|
||||||
|
email: email,
|
||||||
|
};
|
||||||
|
const url = `/User/LogoutUser`;
|
||||||
|
const response = await PatchData<User>(url, request);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateExistance = async (email: string): Promise<UserExistence> => {
|
||||||
|
const url = `/User/ValidateExistence`;
|
||||||
|
const request = {
|
||||||
|
email: email,
|
||||||
|
};
|
||||||
|
const response = await postData(url, request);
|
||||||
|
return response as UserExistence;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useUserItems,
|
||||||
|
useUserItem,
|
||||||
|
updateUser,
|
||||||
|
createUser,
|
||||||
|
deleteUser,
|
||||||
|
ChangeUserStatus,
|
||||||
|
getUserByEmail,
|
||||||
|
loginUser,
|
||||||
|
logoutUser,
|
||||||
|
validateExistance,
|
||||||
|
};
|
||||||
38
src/lib/AuthCookie.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import useUserFromCookie from "./useUser";
|
||||||
|
|
||||||
|
export const setCookie = (name: string, value: string, days: number) => {
|
||||||
|
let expires = "";
|
||||||
|
if (days) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
|
expires = "; expires=" + date.toUTCString();
|
||||||
|
}
|
||||||
|
const isSecure = window.location.protocol === "https:";
|
||||||
|
document.cookie = `${name}=${value || ""}${expires}; path=/;${
|
||||||
|
isSecure ? " Secure" : ""
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCookie = (name: string) => {
|
||||||
|
const nameEQ = name + "=";
|
||||||
|
const ca = document.cookie.split(";");
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const eraseCookie = (name: string) => {
|
||||||
|
document.cookie = name + "=; Max-Age=-99999999;";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAllCookies = () => {
|
||||||
|
const cookies = document.cookie.split(";");
|
||||||
|
|
||||||
|
for (let cookie of cookies) {
|
||||||
|
const cookieName = cookie.split("=")[0].trim();
|
||||||
|
document.cookie = `${cookieName}=; Max-Age=-99999999; path=/;`;
|
||||||
|
}
|
||||||
|
};
|
||||||
19
src/lib/Enums.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export enum Status {
|
||||||
|
Active = "Active",
|
||||||
|
Inactive = "Inactive",
|
||||||
|
Deleted = "Deleted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccessLevel {
|
||||||
|
Read = "Read",
|
||||||
|
Write = "Write",
|
||||||
|
All = "All",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Applications {
|
||||||
|
LSAWebPortal = "LSAWebPortal",
|
||||||
|
CustomerDashboard = "CustomerDashboard",
|
||||||
|
Discover = "Discover",
|
||||||
|
LSAMobile = "LSAMobile",
|
||||||
|
BluePrint = "BluePrint",
|
||||||
|
}
|
||||||
7
src/lib/azureConfig.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const config = {
|
||||||
|
appId: process.env.NEXT_PUBLIC_APP_ID ?? "",
|
||||||
|
redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI,
|
||||||
|
scopes: [`${process.env.NEXT_PUBLIC_SCOPE}`],
|
||||||
|
authority: process.env.NEXT_PUBLIC_AUTHORITY,
|
||||||
|
postLogoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URI,
|
||||||
|
};
|
||||||
97
src/lib/interfaces.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { UUID } from "crypto"
|
||||||
|
import { Applications, Status } from "./Enums";
|
||||||
|
|
||||||
|
export interface ILink {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
hide?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SampleItem {
|
||||||
|
id: UUID | null;
|
||||||
|
testName: string;
|
||||||
|
statusCode: "P" | "A" | "D";
|
||||||
|
numValue: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageUrl {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAudit {
|
||||||
|
createdAt: Date;
|
||||||
|
createdBy: string;
|
||||||
|
updatedAt: Date;
|
||||||
|
updatedBy: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User extends IAudit {
|
||||||
|
id: string | null;
|
||||||
|
guid: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
middleName: string;
|
||||||
|
lastName: string;
|
||||||
|
displayName: string;
|
||||||
|
roleId: string;
|
||||||
|
companies: string[];
|
||||||
|
projects: string[];
|
||||||
|
lastLogIn: Date | null;
|
||||||
|
lastLogOut: Date | null;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeStatusRequest {
|
||||||
|
id: string;
|
||||||
|
status: Status;
|
||||||
|
}
|
||||||
|
export interface Role extends IAudit {
|
||||||
|
id: string | null;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
applications: string[];
|
||||||
|
permissions: string[];
|
||||||
|
modules: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Permission extends IAudit {
|
||||||
|
id: string | null;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
accessLevel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserClaims {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
applications: string[];
|
||||||
|
modules: Module[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserData {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
roleId: string;
|
||||||
|
roleName: string | null;
|
||||||
|
lastLogin: Date | null;
|
||||||
|
lastLogout: Date | null;
|
||||||
|
modules: Module[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Module extends IAudit {
|
||||||
|
id: string | null;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: string | null;
|
||||||
|
route: string;
|
||||||
|
order: number | null;
|
||||||
|
application: Applications;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserExistence {
|
||||||
|
existence: boolean;
|
||||||
|
}
|
||||||
18
src/lib/msalInstance.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { PublicClientApplication } from "@azure/msal-browser";
|
||||||
|
import { config } from "./azureConfig";
|
||||||
|
|
||||||
|
const msalConfig = {
|
||||||
|
auth: {
|
||||||
|
clientId: config.appId,
|
||||||
|
redirectUri: config.redirectUri,
|
||||||
|
authority: config.authority,
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
cacheLocation: "localStorage",
|
||||||
|
storeAuthStateInCookie: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const msalInstance = new PublicClientApplication(msalConfig);
|
||||||
|
|
||||||
|
export default msalInstance;
|
||||||
54
src/lib/useUser.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { UserData } from "@/lib/interfaces";
|
||||||
|
import { getCookie, setCookie } from "./AuthCookie";
|
||||||
|
|
||||||
|
const fetchUserFromCookie = () => {
|
||||||
|
const user = getCookie("user");
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useUserFromCookie = ({ redirectTo = "" } = {}) => {
|
||||||
|
const {
|
||||||
|
data: user,
|
||||||
|
mutate: mutateUser,
|
||||||
|
error,
|
||||||
|
} = useSWR("user", fetchUserFromCookie);
|
||||||
|
|
||||||
|
const [userData, setUserData] = useState<UserData | null>(null);
|
||||||
|
const isLoadingUser = !user && !error;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (user) {
|
||||||
|
const userData = user ? (JSON.parse(user ?? "") as UserData) : null;
|
||||||
|
setUserData(userData);
|
||||||
|
} else {
|
||||||
|
setUserData(null);
|
||||||
|
if (window.location.pathname !== "/") window.location.href = "/";
|
||||||
|
}
|
||||||
|
}, 100); // 100ms delay
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (redirectTo && !isLoadingUser && !user) {
|
||||||
|
window.location.href = redirectTo;
|
||||||
|
}
|
||||||
|
}, [redirectTo, user, isLoadingUser]);
|
||||||
|
|
||||||
|
const setUser = (newUserData: UserData | null) => {
|
||||||
|
if (newUserData) {
|
||||||
|
setUserData(newUserData);
|
||||||
|
mutateUser();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { userData, token: user, error, mutateUser, isLoadingUser, setUser };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserFromCookie;
|
||||||
81
src/services/api.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";
|
||||||
|
|
||||||
|
interface ApiError {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
throw new Error("Unexpected response status");
|
||||||
|
},
|
||||||
|
(error: AxiosError<ApiError>) => {
|
||||||
|
if (error.response) {
|
||||||
|
console.error("Response Error:", error.response.data);
|
||||||
|
return Promise.reject(error.response.data);
|
||||||
|
} else if (error.request) {
|
||||||
|
console.error("Request Error:", error.request);
|
||||||
|
return Promise.reject("No response received from server");
|
||||||
|
} else {
|
||||||
|
console.error("Error:", error.message);
|
||||||
|
return Promise.reject("Error in request configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestData = async <T>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.request<T>({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getData = async <T>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("GET", url, undefined, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postData = async <T>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("POST", url, data, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putData = async <T>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("PUT", url, data, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteData = async (
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: any
|
||||||
|
): Promise<void> => {
|
||||||
|
return await requestData("DELETE", url, data, config);
|
||||||
|
};
|
||||||
113
src/services/cerberos-api.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { getCookie, setCookie } from "@/lib/AuthCookie";
|
||||||
|
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";
|
||||||
|
import { refreshToken } from "./cerberos-services";
|
||||||
|
|
||||||
|
interface ApiError {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_CERBEROS_API_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
async (config) => {
|
||||||
|
await refreshToken();
|
||||||
|
const token = getCookie("authToken");
|
||||||
|
|
||||||
|
if (token && !config.headers.Authorization) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
if (response.status >= 200 && response.status < 3600) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
throw new Error("Unexpected response status");
|
||||||
|
},
|
||||||
|
(error: AxiosError<ApiError>) => {
|
||||||
|
if (error.response) {
|
||||||
|
console.error("Response Error:", error.response.data);
|
||||||
|
return Promise.reject(error.response.data);
|
||||||
|
} else if (error.request) {
|
||||||
|
console.error("Request Error:", error.request);
|
||||||
|
return Promise.reject("No response received from server");
|
||||||
|
} else {
|
||||||
|
console.error("Error:", error.message);
|
||||||
|
return Promise.reject("Error in request configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestData = async <T>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.request<T>({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getData = async <T>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("GET", url, undefined, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDataToken = async <T>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("GET", url, undefined, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postData = async <T>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("POST", url, data, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putData = async <T>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("PUT", url, data, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteData = async (
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: any
|
||||||
|
): Promise<void> => {
|
||||||
|
return await requestData("DELETE", url, data, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PatchData = async <T>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return await requestData<T>("PATCH", url, data, config);
|
||||||
|
};
|
||||||
97
src/services/cerberos-services.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { deleteAllCookies, getCookie, setCookie } from "@/lib/AuthCookie";
|
||||||
|
import { User } from "@/lib/interfaces";
|
||||||
|
import useUserFromCookie from "@/lib/useUser";
|
||||||
|
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_CERBEROS_API_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorAlert = async (message: any) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
toast.warn(message, {
|
||||||
|
position: "top-center",
|
||||||
|
onClose: resolve,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestData = async <T,>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.request<T>({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAccessToken = async (token: string): Promise<User> => {
|
||||||
|
const url = `/Authentication/GenerateToken`;
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const response = await getData(url, config);
|
||||||
|
return response as User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshAccessToken = async (token: string): Promise<string> => {
|
||||||
|
const url = `/Authentication/RefreshToken`;
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getData(url, config);
|
||||||
|
return response as string;
|
||||||
|
} catch (error) {
|
||||||
|
await errorAlert("Unable to refresh token or session expired");
|
||||||
|
deleteAllCookies();
|
||||||
|
// setUser(null);
|
||||||
|
window.location.href = "/";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshToken = async () => {
|
||||||
|
const token = getCookie("authToken");
|
||||||
|
if (token) {
|
||||||
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||||
|
const expiration = payload.exp;
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
if (expiration - currentTime <= 300) {
|
||||||
|
const newToken = await refreshAccessToken(token);
|
||||||
|
if (newToken) {
|
||||||
|
setCookie("authToken", await newToken, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getData = async <T,>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> => {
|
||||||
|
return requestData<T>("GET", url, undefined, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAccessToken,
|
||||||
|
refreshToken,
|
||||||
|
};
|
||||||
20
tailwind.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
backgroundImage: {
|
||||||
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||||
|
"gradient-conic":
|
||||||
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@images/*": ["public/images/*"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||