mirror of
https://github.com/aaronleetw/savage-tracking.git
synced 2024-11-14 11:01:39 -08:00
added docker
This commit is contained in:
parent
1b9da2fe8b
commit
9e472c8026
15 changed files with 202 additions and 37 deletions
40
.dockerignore
Normal file
40
.dockerignore
Normal file
|
@ -0,0 +1,40 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
68
Dockerfile
Normal file
68
Dockerfile
Normal file
|
@ -0,0 +1,68 @@
|
|||
FROM node:18.19-alpine 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
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
COPY prisma/ ./
|
||||
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 --frozen-lockfile; \
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
# set hostname to localhost
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||
CMD ["node", "server.js"]
|
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
|
@ -0,0 +1,46 @@
|
|||
# docker-compose.dev.yml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
web:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
volumes:
|
||||
- .:/app/web
|
||||
environment:
|
||||
POSTGRES_ADDR: postgres
|
||||
POSTGRES_DATABASE: savage_tracking
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/savage_tracking?schema=public
|
||||
JWT_SECRET: secret
|
||||
TZ: Asia/Taipei
|
||||
RFID_PKEY: secret
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
ports:
|
||||
- 3000:3000
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: savage_tracking
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "pg_isready", "-U", "postgres", "-d", "savage_tracking"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
ports:
|
||||
- 3001:5432
|
||||
|
||||
volumes:
|
||||
pgdata: {}
|
|
@ -17,6 +17,11 @@ const config = {
|
|||
locales: ["en"],
|
||||
defaultLocale: "en",
|
||||
},
|
||||
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
output: 'standalone', // for docker
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export default function Periods() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
Object.keys(periods.data ?? {}).map((date) => {
|
||||
periodCnt = 0;
|
||||
return (
|
||||
<tr className="*:p-1 *:border" key={date}>
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { api } from "~/utils/api";
|
||||
import { AddAttendance } from "~/utils/types";
|
||||
|
||||
export default function ListView() {
|
||||
const today = new Date();
|
||||
|
||||
const [startDate, setStartDate] = useState(`${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`);
|
||||
const [endDate, setEndDate] = useState(`${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`);
|
||||
const [startDate, setStartDate] = useState<string>("");
|
||||
const [endDate, setEndDate] = useState<string>("");
|
||||
const attendanceData = api.admin.getAttendanceFromRange.useQuery({
|
||||
start: startDate,
|
||||
end: endDate
|
||||
|
@ -24,6 +22,12 @@ export default function ListView() {
|
|||
attendanceData.refetch();
|
||||
}).catch((err) => console.log(err));
|
||||
|
||||
useEffect(() => {
|
||||
const today = new Date();
|
||||
setStartDate(today.toISOString().split("T")[0]!)
|
||||
setEndDate(today.toISOString().split("T")[0]!)
|
||||
}, [])
|
||||
|
||||
|
||||
return <div className="block">
|
||||
<div className="flex gap-5 items-center mb-5">
|
||||
|
@ -47,8 +51,9 @@ export default function ListView() {
|
|||
</label>
|
||||
<button className="p-2 px-3 h-fit bg-emerald-600 text-white rounded-lg"
|
||||
onClick={() => {
|
||||
setStartDate(`${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`);
|
||||
setEndDate(`${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`);
|
||||
const today = new Date();
|
||||
setStartDate(today.toISOString().split("T")[0]!);
|
||||
setEndDate(today.toISOString().split("T")[0]!);
|
||||
}}>Today</button>
|
||||
{
|
||||
(attendanceData.isLoading) && <div role="status">
|
||||
|
|
|
@ -37,36 +37,36 @@ export default function UserView() {
|
|||
<label className="block max-w-32">
|
||||
<span className="text-gray-700">Grade</span>
|
||||
<input type="text" className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50 bg-gray-200"
|
||||
disabled value={userData.data?.grade || ""}>
|
||||
disabled value={userData.data?.grade ?? ""}>
|
||||
</input>
|
||||
</label>
|
||||
<label className="block max-w-32">
|
||||
<span className="text-gray-700">Class</span>
|
||||
<input type="text" className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50 bg-gray-200"
|
||||
disabled value={userData.data?.class || ""}>
|
||||
disabled value={userData.data?.class ?? ""}>
|
||||
</input>
|
||||
</label>
|
||||
<label className="block max-w-32">
|
||||
<span className="text-gray-700">Number</span>
|
||||
<input type="text" className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50 bg-gray-200"
|
||||
disabled value={userData.data?.number || ""}>
|
||||
disabled value={userData.data?.number ?? ""}>
|
||||
</input>
|
||||
</label>
|
||||
<label className="block max-w-32">
|
||||
<span className="text-gray-700">Name</span>
|
||||
<input type="text" className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50 bg-gray-200"
|
||||
disabled value={userData.data?.name || ""}>
|
||||
disabled value={userData.data?.name ?? ""}>
|
||||
</input>
|
||||
</label>
|
||||
<label className="block max-w-32">
|
||||
<span className="text-gray-700">Display Name</span>
|
||||
<input type="text" className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50 bg-gray-200"
|
||||
disabled value={userData.data?.dname || ""}>
|
||||
disabled value={userData.data?.dname ?? ""}>
|
||||
</input>
|
||||
</label>
|
||||
|
||||
{
|
||||
(userSelectedPeriods.isLoading || userData.isLoading) && <div role="status">
|
||||
(userSelectedPeriods.isLoading ?? userData.isLoading) && <div role="status">
|
||||
<svg aria-hidden="true" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-emerald-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
|
@ -99,11 +99,11 @@ export default function UserView() {
|
|||
</div>
|
||||
<div className={[
|
||||
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col",
|
||||
(userActualAttendTime.data || 0) + (userAttendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
|
||||
(userActualAttendTime.data ?? 0) + (userAttendTime.data ?? 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
|
||||
].join(" ")}>
|
||||
<div className="text-center text-xl font-bold">預估總時數</div>
|
||||
<div className="flex-grow flex items-center">
|
||||
<div className="text-center text-6xl font-bold">{((userActualAttendTime.data || 0) + (userAttendTime.data || 0)).toFixed(1)}</div>
|
||||
<div className="text-center text-6xl font-bold">{((userActualAttendTime.data ?? 0) + (userAttendTime.data ?? 0)).toFixed(1)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -121,7 +121,7 @@ export default function UserView() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
Object.keys(periods.data ?? {}).map((date) => {
|
||||
periodCnt = 0;
|
||||
const borderTop = new Date(date).setHours(0, 0, 0, 0) > new Date().getTime() && !passedDate;
|
||||
if (borderTop) passedDate = true;
|
||||
|
@ -140,7 +140,7 @@ export default function UserView() {
|
|||
return <td key={timePeriod.id * periodCnt} className="bg-emerald-200 hover:cursor-pointer"
|
||||
onClick={() => userToggleAttendance.mutateAsync({
|
||||
username: username,
|
||||
periodId: thisPeriodId || -1,
|
||||
periodId: thisPeriodId ?? -1,
|
||||
attendance: false
|
||||
}).then(() => {
|
||||
userSelectedPeriods.refetch();
|
||||
|
@ -153,7 +153,7 @@ export default function UserView() {
|
|||
return <td key={timePeriod.id * periodCnt} className="bg-sky-100 hover:cursor-pointer"
|
||||
onClick={() => userToggleAttendance.mutateAsync({
|
||||
username: username,
|
||||
periodId: thisPeriodId || -1,
|
||||
periodId: thisPeriodId ?? -1,
|
||||
attendance: true
|
||||
}).then(() => {
|
||||
userSelectedPeriods.refetch();
|
||||
|
@ -185,7 +185,7 @@ export default function UserView() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
Object.keys(periods.data ?? {}).map((date) => {
|
||||
periodCnt = 0;
|
||||
entered = false;
|
||||
const borderTop = new Date(date).setHours(0, 0, 0, 0) > new Date().getTime() && !passedDate;
|
||||
|
@ -206,7 +206,7 @@ export default function UserView() {
|
|||
const thisPeriodEnd = new Date(date);
|
||||
thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0)
|
||||
|
||||
for (let i = attCnt; i < (userAttendance.data || []).length; i++) {
|
||||
for (let i = attCnt; i < (userAttendance.data ?? []).length; i++) {
|
||||
const thisAtt = userAttendance.data![i];
|
||||
if (thisAtt?.datetime! < thisPeriodStart) continue;
|
||||
if (thisAtt?.datetime! > thisPeriodEnd) {
|
||||
|
|
|
@ -73,7 +73,7 @@ export default function Dash() {
|
|||
if (lastRfid.data === "") return;
|
||||
setUserRfid.mutateAsync({
|
||||
username: user.username,
|
||||
rfid: lastRfid.data || ""
|
||||
rfid: lastRfid.data ?? ""
|
||||
}).then(() => users.refetch())
|
||||
}}>
|
||||
Select
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function Dash() {
|
|||
}}>
|
||||
<option value="">Select a date</option>
|
||||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
Object.keys(periods.data ?? {}).map((date) => {
|
||||
return (<option key={date}>{date}</option>)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -51,11 +51,11 @@ export default function Dash() {
|
|||
</div>
|
||||
<div className={[
|
||||
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
|
||||
(actualAttendTime.data || 0) + (attendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
|
||||
(actualAttendTime.data ?? 0) + (attendTime.data ?? 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
|
||||
].join(" ")}>
|
||||
<div className="text-center text-xl font-bold">預估總時數</div>
|
||||
<div className="flex-grow flex items-center">
|
||||
<div className="text-center text-6xl font-bold">{((actualAttendTime.data || 0) + (attendTime.data || 0)).toFixed(1)}</div>
|
||||
<div className="text-center text-6xl font-bold">{((actualAttendTime.data ?? 0) + (attendTime.data ?? 0)).toFixed(1)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,7 +73,7 @@ export default function Dash() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
Object.keys(periods.data || {}).map((date) => {
|
||||
Object.keys(periods.data ?? {}).map((date) => {
|
||||
periodCnt = 0;
|
||||
entered = false;
|
||||
return (
|
||||
|
@ -88,7 +88,7 @@ export default function Dash() {
|
|||
if (mySelectedPeriods.data?.findIndex((period) => period == thisPeriodId) != -1) {
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-emerald-200 hover:cursor-pointer"
|
||||
onClick={() => toggleAttendance.mutateAsync({
|
||||
periodId: thisPeriodId || -1,
|
||||
periodId: thisPeriodId ?? -1,
|
||||
attendance: false
|
||||
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch()).catch((e) => alert(e.message))}>
|
||||
Will Attend
|
||||
|
@ -96,7 +96,7 @@ export default function Dash() {
|
|||
} else {
|
||||
return <td key={timePeriod.id * periodCnt} className="bg-sky-100 hover:cursor-pointer"
|
||||
onClick={() => toggleAttendance.mutateAsync({
|
||||
periodId: thisPeriodId || -1,
|
||||
periodId: thisPeriodId ?? -1,
|
||||
attendance: true
|
||||
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch()).catch((e) => alert(e.message))}></td>
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ export default function Dash() {
|
|||
const thisPeriodEnd = new Date(date);
|
||||
thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0)
|
||||
|
||||
for (let i = attCnt; i < (myAttendance.data || []).length; i++) {
|
||||
for (let i = attCnt; i < (myAttendance.data ?? []).length; i++) {
|
||||
const thisAtt = myAttendance.data![i];
|
||||
if (thisAtt?.datetime! < thisPeriodStart) continue;
|
||||
if (thisAtt?.datetime! > thisPeriodEnd) {
|
||||
|
|
|
@ -15,7 +15,7 @@ export async function myCreateContext(opts: CreateNextContextOptions) {
|
|||
// Verify JWT
|
||||
let session = PublicUserType.parse(undefined);
|
||||
try {
|
||||
jwt.verify(token || "", process.env.JWT_SECRET || "", (err, decoded) => {
|
||||
jwt.verify(token ?? "", process.env.JWT_SECRET ?? "", (err, decoded) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
|
|
|
@ -18,10 +18,10 @@ export const adminRouter = createTRPCRouter({
|
|||
username: input.username,
|
||||
}
|
||||
}).then((user) => {
|
||||
const result = compareSync(input.password, user?.password || "");
|
||||
const result = compareSync(input.password, user?.password ?? "");
|
||||
if (result) {
|
||||
const session = PublicUserType.parse(user)!;
|
||||
const token = jwt.sign(session, process.env.JWT_SECRET || "", { expiresIn: "1d" });
|
||||
const token = jwt.sign(session, process.env.JWT_SECRET ?? "", { expiresIn: "1d" });
|
||||
ctx.res.setHeader("Set-Cookie", `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${60 * 60 * 24};`);
|
||||
return {
|
||||
status: "success",
|
||||
|
@ -57,7 +57,7 @@ export const adminRouter = createTRPCRouter({
|
|||
username: ctx.session?.username,
|
||||
}
|
||||
}).then(async (user) => {
|
||||
const result = compareSync(input.oldPassword, user?.password || "");
|
||||
const result = compareSync(input.oldPassword, user?.password ?? "");
|
||||
if (result) {
|
||||
await ctx.db.user.update({
|
||||
where: {
|
||||
|
@ -241,7 +241,7 @@ export const adminRouter = createTRPCRouter({
|
|||
timePeriods.forEach(async (timePeriod) => {
|
||||
await ctx.db.period.create({
|
||||
data: {
|
||||
date: new Date(input.date),
|
||||
date: new Date(input.date.replace(/-/g, "/")),
|
||||
timePeriodId: timePeriod.id,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ export const createTRPCContext = (_opts: CreateNextContextOptions) => {
|
|||
// Verify JWT
|
||||
let session = PublicUserType.parse(undefined);
|
||||
try {
|
||||
jwt.verify(token || "", process.env.JWT_SECRET || "", (err, decoded) => {
|
||||
jwt.verify(token ?? "", process.env.JWT_SECRET ?? "", (err, decoded) => {
|
||||
if (err) {
|
||||
session = undefined;
|
||||
} else {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "preserve",
|
||||
"plugins": [{ "name": "next" }],
|
||||
"incremental": true,
|
||||
|
|
Loading…
Reference in a new issue