Compare commits

..

54 Commits

Author SHA1 Message Date
13f9d2647b change tracking script 2024-05-16 14:49:44 -05:00
94f70c2647 change to plausible 2024-05-06 09:26:19 -05:00
d65a81ccc9 update script 2024-04-26 17:39:02 -05:00
51ecbd5b38 make min machines 1 2024-04-23 16:48:42 -05:00
54831e55d4 add api healthcheck 2024-04-23 16:34:36 -05:00
c83769e6e0 add health check 2024-04-23 16:28:30 -05:00
2a41854483 use shared 2024-04-23 16:01:14 -05:00
209cb9eb16 update tracking url 2024-04-23 15:35:12 -05:00
9f086bd561 add fly config 2024-04-22 12:57:51 -05:00
59c10140a8 add pull policy to build 2024-04-05 10:37:11 -05:00
2322c320a6 use client side variable for environment 2024-04-05 10:21:54 -05:00
3064db8df7 add analytics script 2024-04-05 10:13:58 -05:00
e910515270 add configurable port 2024-02-29 17:16:39 -06:00
Brandon Egger
832a77f109
Merge pull request #6 from brandonegg/dokkku-deploy
Docker deployment
2024-02-28 14:59:14 -06:00
dc1451bd2e add sharp 2024-02-28 14:54:36 -06:00
5f9432a427 remove RUNNER_ENV 2024-02-28 14:53:19 -06:00
bef6bd691d change base 2024-02-28 14:38:48 -06:00
a2c340cd15 new improved dockerfile 2024-02-28 14:29:29 -06:00
a8d4a64b7b revert config 2024-02-28 14:25:11 -06:00
0e34734a45 add dockerignore 2024-02-28 14:23:29 -06:00
5395028cc8 skip ts errors in build 2024-02-28 14:21:44 -06:00
b5e6551692 add eslint ignore during builds 2024-02-28 14:16:30 -06:00
2a3d310389 whatver 2024-02-21 16:57:39 -06:00
c487bbe003 add skip env validation 2024-02-21 16:48:08 -06:00
92d18426b9 add arbitrary values 2024-02-21 16:33:26 -06:00
77a8137342 change to stack.env 2024-02-21 16:06:54 -06:00
a02264b78b update next packages 2024-02-21 15:51:49 -06:00
0d67624b91 pass build args 2024-02-21 14:33:32 -06:00
a4c813069a add arg 2024-02-20 13:11:43 -06:00
b63e63203e add missing env 2024-02-20 13:09:37 -06:00
4da489ba52 redeclare build args in specific stage 2024-02-20 13:05:08 -06:00
f39bfd9980 new attempt 2024-02-20 12:51:41 -06:00
364a8a43c5 update with seprate production and dev runners 2024-02-20 12:33:48 -06:00
8cacef2268 add updated dockerfile for dokku 2024-02-20 12:02:53 -06:00
Brandon Egger
954c11101c fix type errors 2023-09-05 23:38:04 -05:00
Brandon Egger
ee86d453ba
Merge pull request #5 from brandonegg/brandonegg-patch-1
Update README.md
2023-09-05 23:29:04 -05:00
Brandon Egger
48e3eb49d2
Update README.md 2023-09-05 23:28:41 -05:00
Brandon Egger
8efb1e045d
Merge pull request #4 from brandonegg/create-resource
Create/Delete Resource Admin Functions
2023-09-05 23:26:32 -05:00
Brandon Egger
23e9f91323 minor styling changes 2023-09-05 21:47:50 -05:00
Brandon Egger
2ef07fd37a add delete function 2023-09-05 20:38:05 -05:00
Brandon Egger
f66c35a225 add confirm button for dangerous admin actions 2023-09-05 20:28:03 -05:00
Brandon Egger
8b42377453 reduce redundant trpc error handling code to function 2023-09-05 20:03:38 -05:00
Brandon Egger
ee7268e724 make input strict 2023-09-05 19:48:02 -05:00
Brandon Egger
cad4b78f47 add input fields for age range 2023-09-05 19:45:32 -05:00
Brandon Egger
634f35657e improve readability of server error for user 2023-09-05 19:45:05 -05:00
Brandon Egger
35c301a686 Merge remote-tracking branch 'origin/main' into create-resource 2023-09-05 18:51:48 -05:00
Brandon Egger
2edc5d57b6 improve resource schema to reflect new icon structure 2023-09-04 00:35:57 -05:00
Brandon Egger
f7144e7cf4 switch to handle submit 2023-09-04 00:25:54 -05:00
Brandon Egger
cd1dc2a555 add create resource trpc function 2023-09-04 00:03:42 -05:00
Brandon Egger
c243fda8e1 fix link add button alignment 2023-08-30 22:22:32 -05:00
Brandon Egger
7fc0895177 fix issue where edit page resource query was refetching causing icon file to be reset 2023-08-30 22:13:41 -05:00
Brandon Egger
6e4efe2842 reuse form to create resource create page 2023-08-30 22:08:51 -05:00
Brandon Egger
2e2f99e0ec add admin bar with create button 2023-08-30 21:48:04 -05:00
Brandon Egger
33b30264d9
Merge pull request #3 from brandonegg/add-next-auth-middleware
add redirect to login when user on page which requires account
2023-08-30 21:37:13 -05:00
29 changed files with 1180 additions and 313 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
.env
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

View File

@ -18,4 +18,7 @@ NEXTAUTH_URL="http://localhost:3000"
DATABASE_URL=
# Admin account details
ADMIN_PASSWORD="password"
ADMIN_PASSWORD="password"
# Container Port
PORT=1234

View File

@ -1,19 +1,61 @@
# Production Environment Dockerfile
FROM node:18-alpine AS base
# base image
FROM node:alpine
# create & set working directory
RUN mkdir -p /app
# Install dependencies only when needed
##### DEPENDENCIES
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# copy source files
COPY . /app
# Install Prisma Client - remove if not using Prisma
# install dependencies
RUN npm install
COPY prisma ./
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml\* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi
##### BUILDER
FROM base AS builder
ARG DATABASE_URL
ARG NEXT_PUBLIC_CLIENTVAR
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then SKIP_ENV_VALIDATION=1 yarn build; \
elif [ -f package-lock.json ]; then SKIP_ENV_VALIDATION=1 npm run build; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && SKIP_ENV_VALIDATION=1 pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
##### RUNNER
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# start app
RUN npm run build
EXPOSE 3000
CMD npm run start
ENV PORT 3000
CMD ["server.js"]

View File

@ -1,33 +1,40 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/874ce895-0572-4817-a6b3-cdff89966cd4/deploy-status)](https://app.netlify.com/sites/auditorytraining/deploys)
# Auditory Training Resources
<p align="center">
<img src="assets/atr_table.png" width="800" />
</p>
## Purpose
There are many auditory training resources that often have unique requirements. A patient may require an Android application, or help with a specific skill. The patient may also already be at a specific skill level, and want a resource that is more challenging or easier depending on age group. The goal of this application is to provide an accessible website for patients and providers to easily find recommendations for auditory training development resources based on their unique requirements.
## Production Deployment
### Docker Method
Currently this is being deployed through docker on a single VPS. Using the included docker-compose you can start the production server with the following command:
```sh
docker-compose up --build -d
```
This should deploy the instance on localhost:1234
I have this hosted behind a reverse proxy (nginx) which routes traffic from the auditorytraining.info URI to localhost:1234. You can of course change this to any port that works best in your configuration!
A more permanent hosting solution through the University is in the works.
## Local Deployment
TODO
### Netlify
## API Access
Documentation and updates to our RPC backend API are planned for the future. As of now there is no official documentation for accessing our resources database. More information to come!
As of recently, the project has been migrated to netlify enabling us to automate the deployment process and provide a more reliable experience for the end user.
## Directory Breakdown
Many directories have associated README's to provide related directory information.
- ***src/*** - source directory for nextjs frontend/backend application.
- ***data/*** - Non-application (application referring to excelProcessor or frontend) specific data such as the ATR data excel sheet which has multiple uses in each application.
- ***prisma/*** - Prisma schema and seeding of admin account (specified in env).
- ***excelProcessor/*** - Main excel processing backend server and CLI utility for uploading the ATR data excel sheet to MongoDB.
- **_src/_** - source directory for nextjs frontend/backend application.
- **_data/_** - Non-application (application referring to excelProcessor or frontend) specific data such as the ATR data excel sheet which has multiple uses in each application.
- **_prisma/_** - Prisma schema and seeding of admin account (specified in env).
- **_excelProcessor/_** - Main excel processing backend server and CLI utility for uploading the ATR data excel sheet to MongoDB.

View File

@ -1,4 +1,5 @@
# Data
Backend data is placed in this repository. Currently this consists of the auditory training resources
Utilize `base_template.xlsx` as your starting point when creating auditory training resource data in excell form. This structure is what the upload script will look for. If your excell sheet is missing any of these required fields the sheet parser will fail and notify you that the upload could not complete.
# Data
Backend data is placed in this repository. Currently this consists of the auditory training resources
Utilize `base_template.xlsx` as your starting point when creating auditory training resource data in excell form. This structure is what the upload script will look for. If your excell sheet is missing any of these required fields the sheet parser will fail and notify you that the upload could not complete.

View File

@ -1,10 +1,16 @@
version: '3'
version: "3"
services:
uiowa-atr:
build: .
build:
context: .
restart: always
pull_policy: build
ports:
- "1234:3000"
- "${PORT}:3000"
environment:
NODE_ENV: production
- NEXTAUTH_SECRET
- NEXTAUTH_URL
- DATABASE_URL
- JWT_SECRET
- NODE_ENV

View File

@ -1,29 +1,39 @@
# Importing Data
The purpose of the import script is to take the formatted excel sheet (found in /data/atr_data.xlsx) and upload it to the remote mongoDB server.
# Tutorial
This script is currently in early stages and was initially designed to simplify uploading the assets until a more permanent web UI could be built for the task. Therefore this utility is very low-level and likely requires some tinkering outside its intended use case.
## Setup
1. Open import/main.py
2. Configure the two globals:
```python
MONGO_URL = "enter mongo DB URL"
MONGO_DB = "name of DB"
```
3. Make sure dependencies are intalled with `poetry install`
## Run
The script takes one parameter which is simply the path of your excel sheet.
```sh
poetry run python import/main.py $PATH
```
For example; if uploading the data provided in this repository you can run the following:
```sh
poetry run python import/main.py ../data/atr_data.xlsx
```
You should see output showing the individual objects it extracted from your excel sheet along with a success message at the end.
# Expected Data Format
See {project_root}/data/atr_data.xlsx
# Importing Data
The purpose of the import script is to take the formatted excel sheet (found in /data/atr_data.xlsx) and upload it to the remote mongoDB server.
# Tutorial
This script is currently in early stages and was initially designed to simplify uploading the assets until a more permanent web UI could be built for the task. Therefore this utility is very low-level and likely requires some tinkering outside its intended use case.
## Setup
1. Open import/main.py
2. Configure the two globals:
```python
MONGO_URL = "enter mongo DB URL"
MONGO_DB = "name of DB"
```
3. Make sure dependencies are intalled with `poetry install`
## Run
The script takes one parameter which is simply the path of your excel sheet.
```sh
poetry run python import/main.py $PATH
```
For example; if uploading the data provided in this repository you can run the following:
```sh
poetry run python import/main.py ../data/atr_data.xlsx
```
You should see output showing the individual objects it extracted from your excel sheet along with a success message at the end.
# Expected Data Format
See {project_root}/data/atr_data.xlsx

28
fly.toml Normal file
View File

@ -0,0 +1,28 @@
# fly.toml app configuration file generated for uiowa-atr on 2024-04-23T16:22:44-05:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'uiowa-atr'
primary_region = 'ord'
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
processes = ['app']
[[http_service.checks]]
interval = '30s'
timeout = '5s'
grace_period = '10s'
method = 'GET'
path = '/api/healthcheck'
[[vm]]
memory = '256MB'
size = 'shared-cpu-1x'

View File

@ -9,6 +9,7 @@
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
output: "standalone",
/**
* If you have the "experimental: { appDir: true }" setting enabled, then you

696
package-lock.json generated
View File

@ -20,12 +20,13 @@
"argon2": "^0.30.3",
"dotenv": "^16.0.3",
"formidable": "^3.5.0",
"next": "^13.2.1",
"next-auth": "^4.19.0",
"next": "^14.1.0",
"next-auth": "^4.24.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.44.3",
"react-modal": "^3.16.1",
"sharp": "^0.33.2",
"superjson": "1.9.1",
"zod": "^3.20.6"
},
@ -108,6 +109,15 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@emnapi/runtime": {
"version": "0.45.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
"integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -205,6 +215,437 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz",
"integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.1"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz",
"integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.1"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=11",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz",
"integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=10.13",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz",
"integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz",
"integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz",
"integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz",
"integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz",
"integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz",
"integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz",
"integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.1"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz",
"integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.1"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz",
"integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.1"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz",
"integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.1"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz",
"integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.1"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz",
"integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.1"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz",
"integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/runtime": "^0.45.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz",
"integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz",
"integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -282,9 +723,9 @@
}
},
"node_modules/@next/env": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
"integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ=="
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
"integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.4.19",
@ -296,9 +737,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
"integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz",
"integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==",
"cpu": [
"arm64"
],
@ -311,9 +752,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
"integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz",
"integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==",
"cpu": [
"x64"
],
@ -326,9 +767,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
"integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz",
"integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==",
"cpu": [
"arm64"
],
@ -341,9 +782,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
"integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz",
"integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==",
"cpu": [
"arm64"
],
@ -356,9 +797,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
"integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz",
"integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==",
"cpu": [
"x64"
],
@ -371,9 +812,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
"integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz",
"integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==",
"cpu": [
"x64"
],
@ -386,9 +827,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
"integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz",
"integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==",
"cpu": [
"arm64"
],
@ -401,9 +842,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
"integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz",
"integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==",
"cpu": [
"ia32"
],
@ -416,9 +857,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
"integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz",
"integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==",
"cpu": [
"x64"
],
@ -520,9 +961,9 @@
"dev": true
},
"node_modules/@swc/helpers": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
"integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==",
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
"dependencies": {
"tslib": "^2.4.0"
}
@ -1406,9 +1847,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001524",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz",
"integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==",
"version": "1.0.30001588",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
"integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==",
"funding": [
{
"type": "opencollective",
@ -1492,11 +1933,22 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@ -1507,8 +1959,16 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": {
"version": "1.1.3",
@ -2690,11 +3150,6 @@
"node": ">=10.13.0"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/globals": {
"version": "13.21.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
@ -2965,6 +3420,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/is-async-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
@ -3653,35 +4113,34 @@
"dev": true
},
"node_modules/next": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
"integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz",
"integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==",
"dependencies": {
"@next/env": "13.4.19",
"@swc/helpers": "0.5.1",
"@next/env": "14.1.0",
"@swc/helpers": "0.5.2",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
"postcss": "8.4.14",
"styled-jsx": "5.1.1",
"watchpack": "2.4.0",
"zod": "3.21.4"
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=16.8.0"
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "13.4.19",
"@next/swc-darwin-x64": "13.4.19",
"@next/swc-linux-arm64-gnu": "13.4.19",
"@next/swc-linux-arm64-musl": "13.4.19",
"@next/swc-linux-x64-gnu": "13.4.19",
"@next/swc-linux-x64-musl": "13.4.19",
"@next/swc-win32-arm64-msvc": "13.4.19",
"@next/swc-win32-ia32-msvc": "13.4.19",
"@next/swc-win32-x64-msvc": "13.4.19"
"@next/swc-darwin-arm64": "14.1.0",
"@next/swc-darwin-x64": "14.1.0",
"@next/swc-linux-arm64-gnu": "14.1.0",
"@next/swc-linux-arm64-musl": "14.1.0",
"@next/swc-linux-x64-gnu": "14.1.0",
"@next/swc-linux-x64-musl": "14.1.0",
"@next/swc-win32-arm64-msvc": "14.1.0",
"@next/swc-win32-ia32-msvc": "14.1.0",
"@next/swc-win32-x64-msvc": "14.1.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@ -3699,9 +4158,9 @@
}
},
"node_modules/next-auth": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.23.1.tgz",
"integrity": "sha512-mL083z8KgRtlrIV6CDca2H1kduWJuK/3pTS0Fe2og15KOm4v2kkLGdSDfc2g+019aEBrJUT0pPW2Xx42ImN1WA==",
"version": "4.24.6",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.6.tgz",
"integrity": "sha512-djQt3ZEaWEIxcsuh3HTW2uuzLfXMRjHH+ugAsichlQSbH4iA5MRcgMA2HvTNvsDTDLh44tyU72+/gWsxgTbAKg==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@panva/hkdf": "^1.0.2",
@ -3714,7 +4173,7 @@
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "^12.2.5 || ^13",
"next": "^12.2.5 || ^13 || ^14",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
@ -3725,37 +4184,6 @@
}
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/next/node_modules/zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
@ -4125,10 +4553,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
"integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
"dev": true,
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
@ -4752,6 +5179,45 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/sharp": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
"integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.2",
"semver": "^7.5.4"
},
"engines": {
"libvips": ">=8.15.1",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.2",
"@img/sharp-darwin-x64": "0.33.2",
"@img/sharp-libvips-darwin-arm64": "1.0.1",
"@img/sharp-libvips-darwin-x64": "1.0.1",
"@img/sharp-libvips-linux-arm": "1.0.1",
"@img/sharp-libvips-linux-arm64": "1.0.1",
"@img/sharp-libvips-linux-s390x": "1.0.1",
"@img/sharp-libvips-linux-x64": "1.0.1",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.1",
"@img/sharp-libvips-linuxmusl-x64": "1.0.1",
"@img/sharp-linux-arm": "0.33.2",
"@img/sharp-linux-arm64": "0.33.2",
"@img/sharp-linux-s390x": "0.33.2",
"@img/sharp-linux-x64": "0.33.2",
"@img/sharp-linuxmusl-arm64": "0.33.2",
"@img/sharp-linuxmusl-x64": "0.33.2",
"@img/sharp-wasm32": "0.33.2",
"@img/sharp-win32-ia32": "0.33.2",
"@img/sharp-win32-x64": "0.33.2"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -4792,6 +5258,14 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -5437,18 +5911,6 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@ -27,12 +27,13 @@
"argon2": "^0.30.3",
"dotenv": "^16.0.3",
"formidable": "^3.5.0",
"next": "^13.2.1",
"next-auth": "^4.19.0",
"next": "^14.1.0",
"next-auth": "^4.24.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.44.3",
"react-modal": "^3.16.1",
"sharp": "^0.33.2",
"superjson": "1.9.1",
"zod": "^3.20.6"
},

View File

@ -70,7 +70,7 @@ type Photo {
model AuditoryResource {
id String @id @default(auto()) @map("_id") @db.ObjectId
icon String
icon String?
name String
description String
photo Photo?

View File

@ -8,7 +8,7 @@ type ResourcePhotoProps = (
src: string | undefined;
}
| {
src: string;
src: string | undefined;
photo: null;
}
) & { name: string };

View File

@ -49,7 +49,7 @@ export const ResourceInfo = ({
<ResourcePhoto
name={resource.name}
photo={resource.photo}
src={resource.icon}
src={resource.icon ?? undefined}
/>
<span className="block rounded-lg border border-neutral-900 bg-neutral-900 py-[1px] text-center text-white hover:bg-neutral-500 print:hidden">
more info
@ -61,7 +61,7 @@ export const ResourceInfo = ({
<ResourcePhoto
name={resource.name}
photo={resource.photo}
src={resource.icon}
src={resource.icon ?? undefined}
/>
</div>
)}

View File

@ -1,4 +1,6 @@
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { type ButtonHTMLAttributes, useEffect, useState } from "react";
const AdminActionBody = ({
label,
@ -38,17 +40,67 @@ const AdminActionLink = ({
);
};
const AdminActionConfirmButton = ({
label,
onConfirm,
symbol,
type = "button",
}: {
label: string;
onConfirm?: () => void;
symbol: JSX.Element | undefined;
type?: ButtonHTMLAttributes<HTMLButtonElement>["type"];
}) => {
const [isConfirmView, setConfirmView] = useState(false);
useEffect(() => {
if (!isConfirmView) {
return;
}
setTimeout(() => {
if (isConfirmView) {
setConfirmView(false);
}
}, 5000);
}, [isConfirmView]);
if (isConfirmView) {
return (
<AdminActionButton
symbol={<ExclamationCircleIcon className="w-4 animate-ping" />}
label={`Confirm ${label}`}
onClick={onConfirm}
type={type}
/>
);
}
return (
<AdminActionButton
symbol={symbol}
label={label}
onClick={() => {
setConfirmView(true);
}}
/>
);
};
const AdminActionButton = ({
label,
onClick,
symbol,
type = "button",
}: {
label: string;
onClick: () => void;
onClick?: () => void;
symbol: JSX.Element | undefined;
type?: ButtonHTMLAttributes<HTMLButtonElement>["type"];
}) => {
return (
<button
type={type}
className="py-auto group my-auto h-full space-x-2 rounded-lg border border-neutral-400 bg-neutral-800 px-2 hover:border-neutral-800 hover:bg-white"
onClick={onClick}
>
@ -62,4 +114,4 @@ const AdminActionButton = ({
);
};
export { AdminActionLink, AdminActionButton };
export { AdminActionLink, AdminActionButton, AdminActionConfirmButton };

View File

@ -41,12 +41,14 @@ import Modal from "react-modal";
import { type RouterInputs } from "~/utils/api";
import { PlatformLinkButton } from "~/pages/resources/[id]";
import { ResourcePhoto } from "~/components/ResourcePhoto";
import { FieldLabel } from "~/components/forms/inputLabel";
// Required for accessibility
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
Modal.setAppElement("#__next");
export type ResourceUpdateInput = RouterInputs["auditoryResource"]["update"];
export type ResourceCreateInput = RouterInputs["auditoryResource"]["create"];
/**
* Renders the image selector for resource form.
@ -86,15 +88,22 @@ const SelectImageInput = () => {
htmlFor="resource-image-file"
className="bg-whit group relative cursor-pointer overflow-hidden rounded-xl border border-neutral-400 drop-shadow-lg"
>
<ResourcePhoto
name={name ?? "unknown resource logo"}
photo={photo ?? null}
src={icon}
/>
<div className="absolute bottom-0 left-0 right-0 top-0 hidden place-items-center group-hover:grid group-hover:bg-white/70">
<PencilSquareIcon className="w-16 text-black/50" />
</div>
{photo ? (
<>
<ResourcePhoto
name={name ?? "unknown resource logo"}
photo={photo}
src={icon ?? undefined}
/>
<div className="absolute bottom-0 left-0 right-0 top-0 hidden place-items-center group-hover:grid group-hover:bg-white/70">
<PencilSquareIcon className="w-16 text-black/50" />
</div>
</>
) : (
<div className="grid aspect-square place-items-center hover:bg-white/70">
<PencilSquareIcon className="h-16 w-16 text-black/50" />
</div>
)}
</label>
<input
onChange={onChange}
@ -259,15 +268,15 @@ const ResourceLinkSubForm = () => {
<h1 className="text-xl">Links</h1>
<button
type="button"
className="h-6 rounded-full border border-neutral-900 bg-neutral-200 px-2 leading-tight hover:bg-yellow-400"
className="flex h-6 flex-row items-center rounded-full border border-neutral-900 bg-neutral-200 px-2 leading-tight drop-shadow-sm hover:bg-yellow-400"
onClick={() => {
setLinkModalOpen(!linkModalOpen);
}}
>
<span className="my-auto inline-block align-middle text-sm font-normal text-neutral-700">
<span className="text-sm font-normal leading-3 text-neutral-700">
Add
</span>
<PlusIcon className="my-auto inline-block w-4 align-middle" />
<PlusIcon className="w-4 leading-3" />
</button>
</div>
@ -352,16 +361,18 @@ function ResourceSummarySubForm({
</h2>
<span className="text-md">
<InfoInputLine
details={register("manufacturer.name", { required: true })}
details={register("manufacturer.name", {
required: "Field required",
})}
placeholder="manufacturer"
value={resource?.manufacturer?.name ?? ""}
hint="manufacturer"
value={resource?.name}
/>
</span>
<InfoInputLine
details={register("name", { required: true })}
details={register("name", { required: "Field required" })}
placeholder="name"
value={resource?.name ?? ""}
value={resource?.name}
hint="name"
/>
<span className="my-1 block w-full text-center text-xs italic text-neutral-400">
@ -369,12 +380,37 @@ function ResourceSummarySubForm({
</span>
</div>
</div>
<div>
<FieldLabel
heading="Age Range"
subheading="Specify the minimum and maximum age range supported by the resource"
/>
<div className="mt-4 flex flex-row space-x-4">
<GenericInput
type="number"
placeholder="minimum age"
details={register("ages.min", {
required: "Field required",
valueAsNumber: true,
})}
/>
<span className="text-xl">-</span>
<GenericInput
type="number"
placeholder="maximum age"
details={register("ages.max", {
required: "Field required",
valueAsNumber: true,
})}
/>
</div>
</div>
<MultiSelectorMany
details={register("payment_options", { required: true })}
details={register("payment_options", { required: "Field required" })}
label="Price Category"
defaultValues={
resource?.payment_options ?? [PaymentType.FREE.toString()]
}
defaultValues={resource?.payment_options ?? []}
>
<PaymentTypeOption type={PaymentType.FREE} label="Free" />
<PaymentTypeOption
@ -388,7 +424,7 @@ function ResourceSummarySubForm({
</MultiSelectorMany>
<MultiSelectorMany
details={register("skill_levels", { required: true })}
details={register("skill_levels", { required: "Field required" })}
label="Skill Level"
defaultValues={resource?.skill_levels ?? []}
>
@ -404,7 +440,7 @@ function ResourceSummarySubForm({
</MultiSelectorMany>
<MultiSelectorMany
details={register("skills", { required: true })}
details={register("skills", { required: "Field required" })}
label="Skills Covered"
defaultValues={resource?.skills ?? []}
>
@ -446,7 +482,7 @@ const ResourceDescriptionSubForm = () => {
<ChevronDownIcon className="mx-2 my-auto w-4 text-white group-hover:animate-bounce" />
</button>
<textarea
{...register("description", { required: true })}
{...register("description", { required: "Field required" })}
className={
"h-48 w-full rounded-b-xl p-2" + (dropdownOpen ? " hidden" : "")
}

View File

@ -0,0 +1,16 @@
export const FieldLabel = ({
heading,
subheading,
}: {
heading: string;
subheading: string;
}) => {
return (
<div>
<label className="text-md block font-semibold">{heading}</label>
<span className="block text-sm italic text-neutral-400">
{subheading}
</span>
</div>
);
};

View File

@ -4,6 +4,7 @@ import {
useFormContext,
type UseFormRegisterReturn,
} from "react-hook-form";
import { FieldLabel } from "./inputLabel";
// generics
interface ToStringable {
@ -60,10 +61,7 @@ function MultiSelectorMany<T extends ToStringable>({
<SelectorContext.Provider value={{ type: "many", updateCallback }}>
<SelectedManyContext.Provider value={selected}>
<div className="flex flex-col">
<label className="text-md block font-semibold">{label}</label>
<span className="block text-sm italic text-neutral-400">
Select all that apply
</span>
<FieldLabel heading={label} subheading="Select all that apply" />
<input {...details} readOnly type="text" className="hidden" />
<div className="mt-2 space-x-2 space-y-2 overflow-x-auto">
{children}
@ -100,10 +98,7 @@ function MultiSelector<T extends ToStringable>({
>
<SelectedUniqueContext.Provider value={selected}>
<div className="flex flex-col">
<label className="text-md block font-semibold">{label}</label>
<span className="block text-sm italic text-neutral-400">
Select one from below
</span>
<FieldLabel heading={label} subheading="Select one from below" />
<input {...details} readOnly type="text" className="hidden" />
<div className="space-x-2 space-y-2 overflow-x-auto">{children}</div>
</div>

View File

@ -14,7 +14,7 @@ function InfoInputLine<TFieldName extends InternalFieldName>({
hint,
details,
}: {
value: string;
value?: string | undefined;
placeholder: string;
hint?: string;
details: UseFormRegisterReturn<TFieldName>;
@ -50,16 +50,18 @@ function GenericInput<TFieldName extends InternalFieldName>({
type = "text",
details,
}: {
label: string;
label?: string;
placeholder?: string;
type: HTMLInputTypeAttribute;
details: UseFormRegisterReturn<TFieldName>;
}) {
return (
<section className="w-full space-y-1">
<label className="text-md block px-1 font-semibold text-neutral-600">
{label}
</label>
{label ? (
<label className="text-md block px-1 font-semibold text-neutral-600">
{label}
</label>
) : undefined}
<input
className="block h-8 w-full rounded-lg border border-neutral-600 px-2 py-1"
{...details}

View File

@ -27,6 +27,7 @@ const server = z.object({
*/
const client = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
NEXT_PUBLIC_ENVIRONMENT: z.enum(["development", "test", "production"]),
});
/**
@ -41,6 +42,7 @@ const processEnv = {
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
JWT_SECRET: process.env.JWT_SECRET,
NEXT_PUBLIC_ENVIRONMENT: process.env.NODE_ENV,
};
// Don't touch the part below

View File

@ -6,6 +6,7 @@ import { api } from "~/utils/api";
import "~/styles/globals.css";
import Head from "next/head";
import { env } from "~/env.mjs";
const MyApp: AppType<{ session: Session | null }> = ({
Component,
@ -20,6 +21,15 @@ const MyApp: AppType<{ session: Session | null }> = ({
content="University of Iowa Center for Auditory Training Resources"
/>
<link rel="icon" href="/favicon.ico" />
{env.NEXT_PUBLIC_ENVIRONMENT === "production" ? (
<>
<script
defer
data-domain="auditorytraining.info"
src="https://analytics.brandonegger.com/js/script.js"
></script>
</>
) : null}
</Head>
<Component {...pageProps} />
</SessionProvider>

View File

@ -0,0 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next";
type ResponseData = {
status: string;
};
export default function handler(
_: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.status(200).json({ status: "ok" });
}

View File

@ -1,6 +1,9 @@
import { XCircleIcon } from "@heroicons/react/20/solid";
import { AdminBarLayout } from "~/components/admin/ControlBar";
import { AdminActionButton, AdminActionLink } from "~/components/admin/common";
import {
AdminActionButton,
AdminActionConfirmButton,
} from "~/components/admin/common";
import Image from "next/image";
import {
ResourceForm,
@ -13,6 +16,7 @@ import { useRouter } from "next/router";
import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
import { QueryWaitWrapper } from "~/components/LoadingWrapper";
import { type AuditoryResource } from "@prisma/client";
import { parseTRPCErrorMessage } from "~/utils/parseTRPCError";
const EditResourcePage = () => {
const router = useRouter();
@ -25,6 +29,9 @@ const EditResourcePage = () => {
retry(_failureCount, error) {
return error.data?.httpStatus !== 404;
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: false,
}
);
@ -37,8 +44,8 @@ const EditResourcePage = () => {
});
const { mutate } = api.auditoryResource.update.useMutation({
onSuccess: async (_resData) => {
if (!data) {
onSuccess: async (resData) => {
if (!resData) {
setServerError("An unexpected error has occured");
return;
}
@ -46,7 +53,9 @@ const EditResourcePage = () => {
setServerError(undefined);
await router.push(`/resources/${data.id}`);
},
onError: (error) => setServerError(error.message),
onError: (error) => {
setServerError(parseTRPCErrorMessage(error.message));
},
});
const onSubmit: SubmitHandler<ResourceUpdateInput> = (data) => {
@ -81,11 +90,15 @@ const EditResourcePage = () => {
onSubmit(formMethods.getValues());
}}
/>,
<AdminActionLink
<AdminActionConfirmButton
key="cancel"
symbol={<XCircleIcon className="w-4" />}
label="Cancel"
href={`/resources/${data.id}`}
onConfirm={() => {
router.push(`/resources/${data.id}`).catch((error) => {
console.error(error);
});
}}
/>,
]}
>

View File

@ -6,10 +6,14 @@ import { type AuditoryResource, type PlatformLink } from "@prisma/client";
import Image from "next/image";
import Link from "next/link";
import { AdminBarLayout } from "~/components/admin/ControlBar";
import { AdminActionLink } from "~/components/admin/common";
import {
AdminActionConfirmButton,
AdminActionLink,
} from "~/components/admin/common";
import { useRouter } from "next/router";
import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
import { QueryWaitWrapper } from "~/components/LoadingWrapper";
import { TrashIcon } from "@heroicons/react/24/outline";
export const PlatformLinkButton = ({
platformLink,
@ -91,6 +95,15 @@ const ResourceViewPage = () => {
}
);
const { mutate: mutateDelete } = api.auditoryResource.delete.useMutation({
onSuccess: async () => {
await router.push(`/resources`);
},
onError: (error) => {
console.error(error);
},
});
const ConditionalView = (data: AuditoryResource) => {
return (
<div className="mx-auto flex max-w-2xl flex-col flex-col-reverse divide-x py-4 sm:flex-row">
@ -122,13 +135,24 @@ const ResourceViewPage = () => {
return (
<HeaderFooterLayout>
<AdminBarLayout
actions={
actions={[
<AdminActionLink
key="edit"
symbol={<PencilSquareIcon className="w-4" />}
label="Edit Page"
href={`${router.asPath}/edit`}
/>
}
/>,
<AdminActionConfirmButton
key="delete"
label="Delete"
symbol={<TrashIcon className="w-4" />}
onConfirm={() => {
mutateDelete({
id,
});
}}
/>,
]}
>
<div className="mb-12">
<QueryWaitWrapper query={resourceQuery} Render={ConditionalView} />

View File

@ -0,0 +1,86 @@
import { XCircleIcon, PlusCircleIcon } from "@heroicons/react/20/solid";
import { useRouter } from "next/router";
import { useState } from "react";
import {
type SubmitHandler,
useForm,
type UseFormReturn,
} from "react-hook-form";
import { AdminBarLayout } from "~/components/admin/ControlBar";
import {
AdminActionButton,
AdminActionConfirmButton,
} from "~/components/admin/common";
import {
type ResourceCreateInput,
ResourceForm,
type ResourceUpdateInput,
} from "~/components/admin/resources/form";
import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
import { api } from "~/utils/api";
import { parseTRPCErrorMessage } from "~/utils/parseTRPCError";
const EditResourcePage = () => {
const router = useRouter();
const formMethods = useForm<ResourceCreateInput>();
const [serverError, setServerError] = useState<string | undefined>(undefined);
const { mutate } = api.auditoryResource.create.useMutation({
onSuccess: async (resData) => {
if (!resData) {
setServerError("An unexpected error has occured");
}
setServerError(undefined);
await router.push(`/resources/${resData.id}`);
},
onError: (error) => {
setServerError(parseTRPCErrorMessage(error.message));
},
});
const onSubmit: SubmitHandler<ResourceCreateInput> = (data) => {
mutate(data);
};
return (
<HeaderFooterLayout>
<AdminBarLayout
actions={[
<AdminActionButton
key="create"
symbol={<PlusCircleIcon className="w-4" />}
label="Create"
onClick={() => {
formMethods
.handleSubmit(onSubmit)()
.catch((error) => console.error(error));
}}
/>,
<AdminActionConfirmButton
key="cancel"
symbol={<XCircleIcon className="w-4" />}
label="Cancel"
onConfirm={() => {
router.push("/resources").catch((error) => {
console.error(error);
});
}}
/>,
]}
>
<div className="mb-12">
<ResourceForm
methods={
formMethods as unknown as UseFormReturn<ResourceUpdateInput>
}
error={serverError}
/>
</div>
</AdminBarLayout>
</HeaderFooterLayout>
);
};
export default EditResourcePage;

View File

@ -1,5 +1,5 @@
import { LinkIcon } from "@heroicons/react/20/solid";
import { PrinterIcon } from "@heroicons/react/24/solid";
import { PrinterIcon, PlusCircleIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { useRouter } from "next/router";
import ResourceTable from "~/components/ResourceTable";
@ -8,6 +8,42 @@ import { parseQueryData } from "~/utils/parseSearchForm";
import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
import { type AuditoryResource } from "@prisma/client";
import { QueryWaitWrapper } from "~/components/LoadingWrapper";
import { AdminBarLayout } from "~/components/admin/ControlBar";
import { AdminActionLink } from "~/components/admin/common";
const PageHeader = ({ printLink }: { printLink: string }) => {
return (
<div className="mb-2 flex flex-row justify-between p-2 print:hidden sm:mb-4 sm:p-4">
<section className="space-y-2">
<h1 className="text-3xl font-bold">All Resources</h1>
<div className="">
<p className="inline">Fill out the </p>
<Link
href="/resources/search"
className="inline rounded-lg border border-neutral-800 bg-neutral-200 px-2 py-[4px] hover:bg-neutral-900 hover:text-white"
>
search form
<LinkIcon className="inline w-4" />
</Link>
<p className="inline">
{" "}
for a list of auditory training resource recommendations.
</p>
</div>
</section>
<section className="mt-auto">
<Link
href={printLink}
className="inline-block whitespace-nowrap rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md print:hidden sm:space-x-2"
>
<span className="hidden sm:inline-block">Print Results</span>
<PrinterIcon className="inline-block w-6" />
</Link>
</section>
</div>
);
};
const Resources = () => {
const router = useRouter();
@ -49,39 +85,22 @@ const Resources = () => {
return (
<HeaderFooterLayout>
<div className="mx-auto mb-12 mt-6 w-full max-w-6xl md:px-2">
<div className="mb-2 flex flex-row justify-between p-2 print:hidden sm:mb-4 sm:p-4">
<section className="space-y-2">
<h1 className="text-3xl font-bold">All Resources</h1>
<div className="">
<p className="inline">Fill out the </p>
<Link
href="/resources/search"
className="inline rounded-lg border border-neutral-800 bg-neutral-200 px-2 py-[4px] hover:bg-neutral-900 hover:text-white"
>
search form
<LinkIcon className="inline w-4" />
</Link>
<p className="inline">
{" "}
for a list of auditory training resource recommendations.
</p>
</div>
</section>
<AdminBarLayout
actions={[
<AdminActionLink
key="cancel"
symbol={<PlusCircleIcon className="w-4" />}
label="Create New"
href={`/resources/create`}
/>,
]}
>
<div className="mx-auto mb-12 mt-6 w-full max-w-6xl md:px-2">
<PageHeader printLink={printLink} />
<section className="mt-auto">
<Link
href={printLink}
className="inline-block whitespace-nowrap rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md print:hidden sm:space-x-2"
>
<span className="hidden sm:inline-block">Print Results</span>
<PrinterIcon className="inline-block w-6" />
</Link>
</section>
<QueryWaitWrapper query={resourceQuery} Render={ConditionalTable} />
</div>
<QueryWaitWrapper query={resourceQuery} Render={ConditionalTable} />
</div>
</AdminBarLayout>
</HeaderFooterLayout>
);
};

View File

@ -16,6 +16,42 @@ const emptyStringToUndefined = (val: string | undefined | null) => {
return val;
};
const AuditoryResourceSchema = z.object({
icon: z.string().min(1).optional().nullable(),
name: z.string().min(1),
description: z.string().min(1),
manufacturer: z.object({
name: z.string().min(1),
required: z.boolean().default(false),
notice: z.string().nullable().transform(emptyStringToUndefined),
}),
ages: z.object({ min: z.number().int(), max: z.number().int() }).refine(
(ages) => {
return ages.min < ages.max;
},
{
message: "Minimum supported age must be less than maximum supported age.",
}
),
skills: z.array(z.nativeEnum(Skill)),
skill_levels: z.array(z.nativeEnum(SkillLevel)),
payment_options: z.array(z.nativeEnum(PaymentType)),
photo: z
.object({
name: z.string(),
data: z.instanceof(Buffer),
})
.nullable(),
platform_links: z
.array(
z.object({
platform: z.nativeEnum(Platform),
link: z.string().min(1),
})
)
.default([]),
});
export const auditoryResourceRouter = createTRPCRouter({
byId: publicProcedure
.input(z.object({ id: z.string() }))
@ -45,47 +81,30 @@ export const auditoryResourceRouter = createTRPCRouter({
return ctx.prisma.auditoryResource.findMany();
}),
update: protectedProcedure
create: protectedProcedure
.input(AuditoryResourceSchema.strict())
.mutation(async ({ input, ctx }) => {
return await ctx.prisma.auditoryResource.create({
data: input,
});
}),
delete: protectedProcedure
.input(
z.object({
id: z.string(),
icon: z.string().min(1).optional(),
name: z.string().min(1).optional(),
description: z.string().min(1).optional(),
manufacturer: z
.object({
name: z.string().min(1),
required: z.boolean(),
notice: z
.string()
.optional()
.nullable()
.transform(emptyStringToUndefined),
})
.optional(),
ages: z
.object({ min: z.number().int(), max: z.number().int() })
.optional(),
skills: z.array(z.nativeEnum(Skill)).optional(),
skill_levels: z.array(z.nativeEnum(SkillLevel)).optional(),
payment_options: z.array(z.nativeEnum(PaymentType)).optional(),
photo: z
.object({
name: z.string(),
data: z.instanceof(Buffer),
})
.nullable()
.optional(),
platform_links: z
.array(
z.object({
platform: z.nativeEnum(Platform),
link: z.string().min(1),
})
)
.optional(),
})
)
.mutation(async ({ input, ctx }) => {
return await ctx.prisma.auditoryResource.delete({
where: {
id: input.id,
},
});
}),
update: protectedProcedure
.input(AuditoryResourceSchema.partial().extend({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
return await ctx.prisma.auditoryResource.update({
where: {

View File

@ -0,0 +1,12 @@
export const parseTRPCErrorMessage = (message: string) => {
try {
const zodErrors = JSON.parse(message) as unknown as { message: string }[];
return zodErrors
.map((error) => {
return error.message;
})
.join(", ");
} catch {
return message;
}
};

View File

@ -5,30 +5,30 @@ const config = {
extend: {
keyframes: {
expand_in_out: {
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.1)' },
"0%, 100%": { transform: "scale(1)" },
"50%": { transform: "scale(1.1)" },
},
slide_left_full: {
'0%': { transform: 'translate(0%)' },
'100%': { transform: 'translate(-50%)' },
"0%": { transform: "translate(0%)" },
"100%": { transform: "translate(-50%)" },
},
slide_right_full: {
'0%': { transform: 'translate(-50%)' },
'100%': { transform: 'translate(0%)' },
"0%": { transform: "translate(-50%)" },
"100%": { transform: "translate(0%)" },
},
wiggle_rotate: {
'0%, 60%, 100%': { transform: 'rotate(0deg)' },
'42%': { transform: 'rotate(-6deg)' },
'50%': { transform: 'rotate(4deg)' },
'6%, 30%': { transform: 'rotate(24deg)' },
'18%': { transform: 'rotate(-12deg)' },
"0%, 60%, 100%": { transform: "rotate(0deg)" },
"42%": { transform: "rotate(-6deg)" },
"50%": { transform: "rotate(4deg)" },
"6%, 30%": { transform: "rotate(24deg)" },
"18%": { transform: "rotate(-12deg)" },
},
},
animation: {
expand_in_out: 'expand_in_out 2s ease-in-out infinite',
slide_search_page: 'slide_left_full 0.5s ease-in-out',
slide_search_page_backwards: 'slide_right_full 0.5s ease-in-out',
hand_wave: 'wiggle_rotate 1.5s ease-in-out infinite',
expand_in_out: "expand_in_out 2s ease-in-out infinite",
slide_search_page: "slide_left_full 0.5s ease-in-out",
slide_search_page_backwards: "slide_right_full 0.5s ease-in-out",
hand_wave: "wiggle_rotate 1.5s ease-in-out infinite",
},
},
},