Compare commits
57 Commits
472ae5252f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
cba647ca27
|
|||
|
f6b18d3f82
|
|||
|
d660fd12fa
|
|||
|
38f769ef67
|
|||
|
b152175499
|
|||
|
e0655a56d7
|
|||
|
3d6de91dc8
|
|||
|
b28aae201a
|
|||
|
51f52c0a1e
|
|||
|
decd9a6945
|
|||
|
bb69b33481
|
|||
|
a7eb7d5037
|
|||
|
96129c1fe9
|
|||
|
b990b04902
|
|||
|
577d97cfcd
|
|||
|
23f37df217
|
|||
|
98a65043c9
|
|||
|
4934f799f5
|
|||
|
2a69ff0f3e
|
|||
|
64ea2e9524
|
|||
|
a29bac1eb2
|
|||
|
9a54c4712e
|
|||
|
ca130dc2b8
|
|||
|
5c739bcfa4
|
|||
|
7e8d8dc523
|
|||
|
60a7753247
|
|||
|
d54649893c
|
|||
|
0f6ec77dc0
|
|||
|
99f9531d32
|
|||
|
3c37fbf59b
|
|||
|
c1a019a461
|
|||
|
5c4abf24bb
|
|||
|
d125687536
|
|||
|
05f3e019a8
|
|||
|
6d6bef8f50
|
|||
|
6129f489dd
|
|||
|
1d834f3a51
|
|||
|
f7bcb2f476
|
|||
|
0cf9d60991
|
|||
|
8f90a5031a
|
|||
|
e9b53e3e82
|
|||
|
826029ace8
|
|||
|
8854022fac
|
|||
|
be42901480
|
|||
|
0889c2426d
|
|||
|
eee12e5872
|
|||
|
63a5ad8a00
|
|||
|
948870852e
|
|||
|
f6e68d4117
|
|||
|
95068907f9
|
|||
|
c00fbbaea2
|
|||
|
9ac18c30b8
|
|||
|
35880519da
|
|||
|
f159e3da60
|
|||
|
490cbf1156
|
|||
|
da9aa6b87b
|
|||
|
65987354fe
|
@@ -4,6 +4,11 @@ Dockerfile
|
||||
minio
|
||||
node_modules
|
||||
sqlite.db
|
||||
sqlite.db-wal
|
||||
sqlite.db-shm
|
||||
.DS_Store
|
||||
dist
|
||||
.env
|
||||
caddy/data/caddy
|
||||
caddy/config/caddy
|
||||
testaction.secret
|
||||
|
||||
10
.eslintrc.json
Normal file
10
.eslintrc.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"prefer-const": "error"
|
||||
}
|
||||
}
|
||||
34
.gitea/workflows/backend.yaml
Normal file
34
.gitea/workflows/backend.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
name: backend-action
|
||||
run-name: ${{ gitea.actor }} is building docker image 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
registry: gitea.cognizata.com
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: gitea.cognizata.com/atapy/sorvor:latest
|
||||
target: app
|
||||
- name: update server
|
||||
uses: https://github.com/appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: 46.102.174.196
|
||||
username: root
|
||||
key: ${{ secrets.SORVOR_KEY }}
|
||||
port: 22
|
||||
script: "docker compose pull && docker compose up -d"
|
||||
command_timeout: 30h
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -6,3 +6,11 @@ dist
|
||||
.env
|
||||
caddy/data/caddy
|
||||
caddy/config/caddy
|
||||
testaction.secret
|
||||
caddy/logs
|
||||
.next
|
||||
sqlite.db-shm
|
||||
sqlite.db-wal
|
||||
user.json
|
||||
user-p.json
|
||||
user-c.json
|
||||
|
||||
@@ -14,6 +14,7 @@ RUN pnpm run build
|
||||
FROM node:20-slim AS app
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV NODE_ENV="production"
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
1. `pnpm install`
|
||||
2. `pnpm initialize_data`
|
||||
3. `docker compose up`
|
||||
4. `docker compose exec crowdsec cscli bouncers add CaddyBouncer` to get api key
|
||||
5. add api key to `CROWDSEC_API_KEY=<KEY>` in `.env` file
|
||||
|
||||
@@ -24,3 +24,6 @@ tasks:
|
||||
- node -r @swc-node/register src/app.ts
|
||||
env:
|
||||
NODE_ENV: production
|
||||
update-server:
|
||||
cmds:
|
||||
- ssh -t sorvor-p "docker compose pull && docker compose up -d"
|
||||
|
||||
120
addMetadata.ts
120
addMetadata.ts
@@ -1,11 +1,36 @@
|
||||
import { db } from "./src/db";
|
||||
import { group, opinion, zone, province } from "./src/schema";
|
||||
import {
|
||||
group,
|
||||
opinion,
|
||||
zone,
|
||||
province,
|
||||
user,
|
||||
userToSelection,
|
||||
} from "./src/schema";
|
||||
import { Groups, Opinions, Provinces, Districts } from "./initialData";
|
||||
import { createBucket, createClient } from "./src/minio";
|
||||
import { Config } from "./src/config";
|
||||
import ud from "./user-c.json";
|
||||
|
||||
const user_data: UserData[] = ud;
|
||||
console.log(ud.length);
|
||||
|
||||
for (const user of user_data) {
|
||||
let thisName = `${user.first_name} ${user.last_name}`;
|
||||
if (
|
||||
user_data.filter((u) => `${u.first_name} ${u.last_name}` == thisName)
|
||||
.length != 1
|
||||
) {
|
||||
console.log(`duplicate name ${user}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await setupBucket();
|
||||
try {
|
||||
await setupBucket();
|
||||
} catch {
|
||||
console.error("Setting up bucket failed");
|
||||
}
|
||||
const isInitialized = await db.query.group
|
||||
.findMany()
|
||||
.then((groups) => groups.length > 0);
|
||||
@@ -31,9 +56,100 @@ async function main() {
|
||||
province: district.province_code,
|
||||
}));
|
||||
await db.insert(zone).values(zoneValues);
|
||||
await create_user();
|
||||
await create_relation();
|
||||
|
||||
const allUser = await db.query.user.findMany({
|
||||
with: {
|
||||
userToSelection: { with: { selection: true } },
|
||||
},
|
||||
});
|
||||
// for (const u of allUser) {
|
||||
// console.log(
|
||||
// u.firstName,
|
||||
// u.lastName,
|
||||
// u.userToSelection.map(
|
||||
// (t) => `${t.selection?.firstName} ${t.selection?.lastName}`,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
type UserData = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
job_code: number;
|
||||
selection: string[];
|
||||
province: string;
|
||||
rank: number;
|
||||
};
|
||||
|
||||
async function create_user() {
|
||||
const provinces = await db.query.province.findMany({});
|
||||
const district = await db.query.zone.findMany({});
|
||||
for (const newUser of user_data) {
|
||||
let isSelectionFound = true;
|
||||
for (const selection of newUser.selection) {
|
||||
const isFound = user_data.findIndex(
|
||||
(p) => `${p.first_name} ${p.last_name}` == selection,
|
||||
);
|
||||
if (isFound == -1) {
|
||||
isSelectionFound = false;
|
||||
}
|
||||
}
|
||||
if (!isSelectionFound) {
|
||||
console.log(newUser.province, newUser, isSelectionFound);
|
||||
} else {
|
||||
await db.insert(user).values({
|
||||
group: newUser.job_code,
|
||||
firstName: newUser.first_name,
|
||||
lastName: newUser.last_name,
|
||||
title: "",
|
||||
cid: "0000000000000",
|
||||
phone: "0000000000",
|
||||
age: 0,
|
||||
job: "",
|
||||
education: "",
|
||||
zone: 1001,
|
||||
rank: newUser.rank,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function create_relation() {
|
||||
const allUser = await db.query.user.findMany({
|
||||
with: {
|
||||
zone: { with: { province: true } },
|
||||
},
|
||||
});
|
||||
for (const u of allUser) {
|
||||
let thisUsers = user_data.filter(
|
||||
(raw) => raw.first_name == u.firstName && raw.last_name == u.lastName,
|
||||
);
|
||||
if (thisUsers.length !== 1) {
|
||||
console.log("duplicated users", thisUsers);
|
||||
return;
|
||||
}
|
||||
const rawUser = thisUsers[0];
|
||||
const selections = allUser.filter(
|
||||
(target) =>
|
||||
rawUser.selection.includes(`${target.firstName} ${target.lastName}`) &&
|
||||
target.zone.province.id == u.zone.province.id,
|
||||
);
|
||||
if (selections.length == 0) {
|
||||
console.log("selection not found", selections);
|
||||
return;
|
||||
}
|
||||
for (const selection of selections) {
|
||||
await db
|
||||
.insert(userToSelection)
|
||||
.values({ userId: u.id, targetId: selection.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBucket() {
|
||||
const BucketPolicy = {
|
||||
Version: "2012-10-17",
|
||||
|
||||
3
app/global.css
Normal file
3
app/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
79
app/grouping/GroupCreator.tsx
Normal file
79
app/grouping/GroupCreator.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { LocationContext } from "@/components/locationContext";
|
||||
import { useContext, useState } from "react";
|
||||
import Grouping from "./Grouping";
|
||||
import { Group, JobCategory, updateGroups } from "./action";
|
||||
|
||||
type Props = {
|
||||
allJobs: JobCategory[];
|
||||
};
|
||||
|
||||
export default function GroupCreator({ allJobs }: Props) {
|
||||
const locationContext = useContext(LocationContext);
|
||||
const [usedJobs, setUsedJobs] = useState<number[]>([]);
|
||||
const [groups, setGroup] = useState<Group[]>(
|
||||
[...Array(4).keys()].map((i) => ({ id: i + 1, jobs: [] })),
|
||||
);
|
||||
function useJob(id: number) {
|
||||
setUsedJobs((u) => [...u, id]);
|
||||
}
|
||||
|
||||
function freeJob(id: number) {
|
||||
setUsedJobs((u) => u.filter((j) => j != id));
|
||||
}
|
||||
|
||||
function updateGroup(id: number, jobs: number[]) {
|
||||
setGroup((groups) =>
|
||||
groups.map((g) => {
|
||||
if (g.id != id) return g;
|
||||
else return { id, jobs };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (
|
||||
locationContext?.zone[0] == undefined ||
|
||||
locationContext?.province[0] == undefined
|
||||
) {
|
||||
alert("ยังไม่ได้เลือกพื้นที่");
|
||||
return;
|
||||
}
|
||||
await updateGroups(
|
||||
locationContext.province[0],
|
||||
locationContext.zone[0],
|
||||
groups,
|
||||
);
|
||||
|
||||
alert("อัพเดทสำเร็จ");
|
||||
}
|
||||
|
||||
if (
|
||||
locationContext?.zone[0] == undefined ||
|
||||
locationContext?.province[0] == undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{groups.map((g) => (
|
||||
<div className="flex flex-col items-center" key={g.id}>
|
||||
<p>สายที่: {g.id}</p>
|
||||
<Grouping
|
||||
availableJobs={allJobs.filter((j) => !usedJobs.includes(j.id))}
|
||||
selectJob={useJob}
|
||||
removeJob={freeJob}
|
||||
updateGroup={(ids) => updateGroup(g.id, ids)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center">
|
||||
<button className="rounded-md bg-green-300 p-2" onClick={submit}>
|
||||
ยืนยัน
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
app/grouping/Grouping.tsx
Normal file
60
app/grouping/Grouping.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState } from "react";
|
||||
import { JobCategory } from "./action";
|
||||
|
||||
type Props = {
|
||||
availableJobs: JobCategory[];
|
||||
selectJob: (id: number) => void;
|
||||
removeJob: (id: number) => void;
|
||||
updateGroup: (id: number[]) => void;
|
||||
};
|
||||
|
||||
export default function Grouping({
|
||||
availableJobs,
|
||||
selectJob,
|
||||
removeJob,
|
||||
updateGroup,
|
||||
}: Props) {
|
||||
const [selectedJob, setSelectedJob] = useState<JobCategory[]>([]);
|
||||
function addJob(id: string) {
|
||||
const _id = parseInt(id);
|
||||
const job = availableJobs.find((j) => j.id == _id);
|
||||
if (job == undefined) return;
|
||||
const newSelectedJob = [...selectedJob, job];
|
||||
setSelectedJob(newSelectedJob);
|
||||
selectJob(_id);
|
||||
updateGroup(newSelectedJob.map((j) => j.id));
|
||||
}
|
||||
function removeJobFromGroup(id: number) {
|
||||
const newSelectedJob = selectedJob.filter((j) => j.id != id);
|
||||
setSelectedJob(newSelectedJob);
|
||||
removeJob(id);
|
||||
updateGroup(newSelectedJob.map((j) => j.id));
|
||||
}
|
||||
return (
|
||||
<div className="m-2 flex w-full flex-col gap-2 rounded-md border border-black p-2 shadow-md">
|
||||
{selectedJob.map((j) => (
|
||||
<div className="flex justify-between gap-2 p-2" key={j.id}>
|
||||
<p>{j.name}</p>
|
||||
<button
|
||||
onClick={() => removeJobFromGroup(j.id)}
|
||||
className="text-red-400"
|
||||
>
|
||||
นำออก
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{selectedJob.length < 5 && (
|
||||
<select onChange={(e) => addJob(e.currentTarget.value)}>
|
||||
<option value={undefined} hidden>
|
||||
None
|
||||
</option>
|
||||
{availableJobs.map((j) => (
|
||||
<option value={j.id} key={j.id}>
|
||||
{j.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
app/grouping/action.ts
Normal file
20
app/grouping/action.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
"use server";
|
||||
|
||||
export type JobCategory = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Group = {
|
||||
id: number;
|
||||
jobs: number[];
|
||||
};
|
||||
|
||||
//TODO: submit group
|
||||
export async function updateGroups(
|
||||
province: number,
|
||||
zone: number,
|
||||
groups: Group[],
|
||||
) {
|
||||
console.log({ province, zone, groups });
|
||||
}
|
||||
24
app/grouping/page.tsx
Normal file
24
app/grouping/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { db } from "@/src/db";
|
||||
import LocationSelector from "../../components/LocationSelector";
|
||||
import LocationContextProvider from "@/components/locationContext";
|
||||
import GroupCreator from "./GroupCreator";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { user } from "@/src/schema";
|
||||
|
||||
export default async function Page() {
|
||||
const provinces = await db.query.province
|
||||
.findMany({ with: { zones: true } })
|
||||
.execute();
|
||||
const jobList = await db.query.group.findMany().execute();
|
||||
const r = await db.query.user
|
||||
.findMany({ columns: { id: true }, where: eq(user.verified, true) })
|
||||
.execute()
|
||||
.then((v) => v.length);
|
||||
console.log(r);
|
||||
return (
|
||||
<LocationContextProvider>
|
||||
<LocationSelector provinces={provinces} />
|
||||
<GroupCreator allJobs={jobList} />
|
||||
</LocationContextProvider>
|
||||
);
|
||||
}
|
||||
89
app/inside/IdComponent.tsx
Normal file
89
app/inside/IdComponent.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
updateIdList: (cids: string[]) => void;
|
||||
};
|
||||
|
||||
export default function IdComponent({ updateIdList }: Props) {
|
||||
const [idSet, setIdSet] = useState<Set<string>>(new Set());
|
||||
const onValidId = (id: string) => {
|
||||
const newSet = new Set(idSet.add(id));
|
||||
setIdSet(newSet);
|
||||
updateIdList([...newSet]);
|
||||
};
|
||||
const removeCid = (id: string) => {
|
||||
const newSet = new Set([...idSet].filter((x) => x !== id));
|
||||
setIdSet(newSet);
|
||||
updateIdList([...newSet]);
|
||||
};
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from(idSet).map((v) => (
|
||||
<FixedId cid={v} removeCid={removeCid} key={v} />
|
||||
))}
|
||||
<SingleIdComponent onValidId={onValidId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type FixedIdProps = {
|
||||
cid: string;
|
||||
removeCid: (cid: string) => void;
|
||||
};
|
||||
|
||||
function FixedId({ cid, removeCid }: FixedIdProps) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<input type="text" className="border-2" disabled value={cid} />
|
||||
<button
|
||||
className="rounded-md bg-red-300 p-2"
|
||||
onClick={() => removeCid(cid)}
|
||||
>
|
||||
ลบ
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SingleIdProps = {
|
||||
onValidId: (cid: string) => void;
|
||||
};
|
||||
|
||||
function SingleIdComponent({ onValidId }: SingleIdProps) {
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [cid, setCid] = useState("");
|
||||
useEffect(() => {
|
||||
const isValidId = isValidThaiID(cid);
|
||||
setIsValid(isValidId);
|
||||
if (isValidId) {
|
||||
onValidId(cid);
|
||||
|
||||
setCid("");
|
||||
}
|
||||
}, [cid, onValidId]);
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="border-2"
|
||||
onChange={(v) => setCid(v.target.value)}
|
||||
value={cid}
|
||||
/>
|
||||
{isValid ? <p>OK</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isValidThaiID(id: string) {
|
||||
if (!/^\d{13}$/.test(id)) {
|
||||
return false;
|
||||
}
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
sum += Number(id[i]) * (13 - i);
|
||||
}
|
||||
const checkSum = (11 - (sum % 11)) % 10;
|
||||
return checkSum === Number(id[12]);
|
||||
}
|
||||
13
app/inside/action.ts
Normal file
13
app/inside/action.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
"use server";
|
||||
import { db } from "@/src/db";
|
||||
import { user } from "@/src/schema";
|
||||
import { inArray } from "drizzle-orm";
|
||||
|
||||
export async function saveUser(cids: string[]) {
|
||||
const rs = await db
|
||||
.update(user)
|
||||
.set({ verified: true })
|
||||
.where(inArray(user.cid, cids))
|
||||
.execute();
|
||||
return rs;
|
||||
}
|
||||
23
app/inside/page.tsx
Normal file
23
app/inside/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { saveUser } from "./action";
|
||||
import IdComponent from "./IdComponent";
|
||||
|
||||
export default function Page() {
|
||||
const [idList, setIdList] = useState<string[]>([]);
|
||||
async function submit() {
|
||||
const rs = await saveUser(idList);
|
||||
alert(`อัพเดทสำเร็จ ${rs.changes} คน`);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<IdComponent updateIdList={(cids) => setIdList(cids)} />
|
||||
<p className="mt-2 flex items-center justify-center gap-4">
|
||||
Total: {idList.length}{" "}
|
||||
<button className="rounded-md bg-green-200 p-2" onClick={submit}>
|
||||
Submit
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
app/layout.tsx
Normal file
17
app/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import "./global.css";
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div className="container mx-auto mt-2 p-2">{children}</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
3
app/page.tsx
Normal file
3
app/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <h1>Hello!</h1>;
|
||||
}
|
||||
47
app/total/TotalSetter.tsx
Normal file
47
app/total/TotalSetter.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { LocationContext } from "@/components/locationContext";
|
||||
import { useContext, useState } from "react";
|
||||
import { updateZone } from "./action";
|
||||
|
||||
export default function TotalSetter() {
|
||||
const locationContext = useContext(LocationContext);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
async function submit() {
|
||||
if (
|
||||
locationContext?.zone[0] == undefined ||
|
||||
locationContext?.province[0] == undefined
|
||||
) {
|
||||
alert("ยังไม่ได้เลือกพื้นที่");
|
||||
return;
|
||||
}
|
||||
await updateZone(
|
||||
locationContext.province[0],
|
||||
locationContext.zone[0],
|
||||
total,
|
||||
);
|
||||
alert(`อัพเดทสำเร็จ`);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="flex">
|
||||
จำนวนทั้งหมด:{" "}
|
||||
<input
|
||||
value={total}
|
||||
className="rounded-md border-2"
|
||||
type="number"
|
||||
onChange={(e) =>
|
||||
setTotal(
|
||||
e.currentTarget.value !== ""
|
||||
? parseInt(e.currentTarget.value)
|
||||
: 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button className="rounded-md bg-green-300 p-2" onClick={submit}>
|
||||
ยืนยัน
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
app/total/action.ts
Normal file
20
app/total/action.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
"use server";
|
||||
|
||||
export type JobCategory = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Group = {
|
||||
id: number;
|
||||
jobs: number[];
|
||||
};
|
||||
|
||||
//TODO: submit group
|
||||
export async function updateZone(
|
||||
province: number,
|
||||
zone: number,
|
||||
total: number,
|
||||
) {
|
||||
console.log({ province, zone, total });
|
||||
}
|
||||
16
app/total/page.tsx
Normal file
16
app/total/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { db } from "@/src/db";
|
||||
import LocationSelector from "../../components/LocationSelector";
|
||||
import LocationContextProvider from "@/components/locationContext";
|
||||
import TotalSetter from "./TotalSetter";
|
||||
|
||||
export default async function Page() {
|
||||
const provinces = await db.query.province
|
||||
.findMany({ with: { zones: true } })
|
||||
.execute();
|
||||
return (
|
||||
<LocationContextProvider>
|
||||
<LocationSelector provinces={provinces} />
|
||||
<TotalSetter />
|
||||
</LocationContextProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
{
|
||||
crowdsec {
|
||||
api_url {$CROWDSEC_API_URL}
|
||||
api_key {$CROWDSEC_API_KEY}
|
||||
}
|
||||
}
|
||||
|
||||
{$CADDY_BASE_HOST} {
|
||||
encode zstd gzip
|
||||
@blocked not remote_ip {$CADDY_ALLOWED_IPS}
|
||||
route {
|
||||
crowdsec
|
||||
respond @blocked "<h1>Access Denied</h1>" 403
|
||||
}
|
||||
encode zstd gzip
|
||||
reverse_proxy http://frontend:3000
|
||||
handle_path /api/* {
|
||||
reverse_proxy http://backend:3000
|
||||
@@ -7,5 +19,9 @@
|
||||
handle_path /profileImages/* {
|
||||
rewrite * /sorvor{path}
|
||||
reverse_proxy http://minio:9000
|
||||
|
||||
}
|
||||
log {
|
||||
output file /var/log/caddy/access.log
|
||||
}
|
||||
}
|
||||
|
||||
14
caddy/Dockerfile
Normal file
14
caddy/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
ARG CADDY_VERSION=2
|
||||
|
||||
FROM caddy:${CADDY_VERSION}-builder-alpine AS builder
|
||||
|
||||
RUN xcaddy build \
|
||||
--with github.com/mholt/caddy-l4 \
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer/http@main \
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer/layer4@main
|
||||
|
||||
FROM caddy:${CADDY_VERSION} AS caddy
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
88
components/LocationSelector/index.tsx
Normal file
88
components/LocationSelector/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { LocationContext } from "../locationContext";
|
||||
|
||||
type Props = {
|
||||
provinces: Province[];
|
||||
};
|
||||
type Province = {
|
||||
id: number;
|
||||
name: string;
|
||||
zones: Zone[];
|
||||
};
|
||||
type Zone = {
|
||||
id: number;
|
||||
name: string;
|
||||
province: number;
|
||||
};
|
||||
export default function LocationSelector({ provinces }: Props) {
|
||||
const [provinceId, setProvinceId] = useState<number | undefined>(undefined);
|
||||
const [amphurList, setAmphurList] = useState<Zone[] | undefined>(undefined);
|
||||
const [amphurId, setAmphurId] = useState<number | undefined>(undefined);
|
||||
const locationContext = useContext(LocationContext);
|
||||
function setProvince(_id: string) {
|
||||
const id = parseInt(_id);
|
||||
setProvinceId(id);
|
||||
const province = provinces.find((p) => p.id == id);
|
||||
if (province == undefined) return;
|
||||
setAmphurList(province.zones);
|
||||
setAmphurId(undefined);
|
||||
}
|
||||
function setAmphur(_id: string) {
|
||||
const id = parseInt(_id);
|
||||
setAmphurId(id);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (locationContext == undefined) return;
|
||||
if (amphurId == undefined || provinceId == undefined) {
|
||||
locationContext.zone[1](undefined);
|
||||
locationContext.province[1](undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
locationContext.zone[1](amphurId);
|
||||
locationContext.province[1](provinceId);
|
||||
}, [amphurId, locationContext, provinceId]);
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
จังหวัด:
|
||||
<select
|
||||
value={provinceId}
|
||||
className="flex-1"
|
||||
onChange={(e) => setProvince(e.currentTarget.value)}
|
||||
>
|
||||
<option value={undefined} hidden>
|
||||
None
|
||||
</option>
|
||||
{provinces.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{amphurList && (
|
||||
<div className="flex gap-2">
|
||||
อำเภอ:
|
||||
<select
|
||||
value={amphurId}
|
||||
className="flex-1"
|
||||
onChange={(e) => setAmphur(e.currentTarget.value)}
|
||||
>
|
||||
<option value={undefined} hidden>
|
||||
None
|
||||
</option>
|
||||
{amphurList.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
components/locationContext.tsx
Normal file
32
components/locationContext.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
import {
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type LocationContextType = {
|
||||
zone: [number | undefined, Dispatch<SetStateAction<number | undefined>>];
|
||||
province: [number | undefined, Dispatch<SetStateAction<number | undefined>>];
|
||||
};
|
||||
|
||||
export const LocationContext = createContext<LocationContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export default function LocationContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const zone = useState<number | undefined>(undefined);
|
||||
const province = useState<number | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<LocationContext.Provider value={{ zone, province }}>
|
||||
{children}
|
||||
</LocationContext.Provider>
|
||||
);
|
||||
}
|
||||
36
compose.yml
36
compose.yml
@@ -1,7 +1,7 @@
|
||||
version: "3"
|
||||
services:
|
||||
backend:
|
||||
restart: always
|
||||
image: gitea.cognizata.com/atapy/sorvor
|
||||
build:
|
||||
target: app # choose a stage to use
|
||||
ports:
|
||||
@@ -9,10 +9,23 @@ services:
|
||||
- 3001:3001
|
||||
volumes:
|
||||
- ./sqlite.db:/app/sqlite.db
|
||||
- ./sqlite.db-shm:/app/sqlite.db-shm
|
||||
- ./sqlite.db-wal:/app/sqlite.db-wal
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
admin:
|
||||
build:
|
||||
dockerfile: ./next.Dockerfile
|
||||
ports:
|
||||
- 3003:3000
|
||||
volumes:
|
||||
- ./sqlite.db:/app/sqlite.db
|
||||
- ./sqlite.db-shm:/app/sqlite.db-shm
|
||||
- ./sqlite.db-wal:/app/sqlite.db-wal
|
||||
frontend:
|
||||
restart: always
|
||||
image: gitea.cognizata.com/atapy/sorvor-front
|
||||
build:
|
||||
context: ../sorvor-front
|
||||
args:
|
||||
@@ -31,7 +44,9 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
caddy:
|
||||
image: caddy
|
||||
build:
|
||||
context: ./caddy/
|
||||
target: caddy
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
@@ -43,3 +58,20 @@ services:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./caddy/data/:/data
|
||||
- ./caddy/config/:/config
|
||||
- ./caddy/logs/:/var/log/caddy/
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec
|
||||
environment:
|
||||
COLLECTIONS: "crowdsecurity/caddy"
|
||||
depends_on:
|
||||
- "caddy"
|
||||
volumes:
|
||||
- ./caddy/logs:/var/log/caddy
|
||||
- crowdsec-db:/var/lib/crowdsec/data/
|
||||
- crowdsec-config:/etc/crowdsec/
|
||||
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
|
||||
|
||||
volumes:
|
||||
crowdsec-db:
|
||||
crowdsec-config:
|
||||
|
||||
4
crowdsec/acquis.yaml
Normal file
4
crowdsec/acquis.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
filenames:
|
||||
- /var/log/caddy/access.log
|
||||
labels:
|
||||
type: caddy
|
||||
@@ -3,16 +3,17 @@ CREATE TABLE `groups` (
|
||||
`name` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `image_to_user` (
|
||||
`user_id` integer PRIMARY KEY NOT NULL,
|
||||
`image_name` text NOT NULL,
|
||||
`created_on` integer DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `opinions` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text DEFAULT '3Choice' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `phone_tokens` (
|
||||
`phone` text PRIMARY KEY NOT NULL,
|
||||
`token` text NOT NULL,
|
||||
`created_on` integer DEFAULT CURRENT_TIMESTAMP
|
||||
`type` text DEFAULT '5Choice' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `provinces` (
|
||||
@@ -24,6 +25,7 @@ CREATE TABLE `users` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`firstName` text NOT NULL,
|
||||
`lastName` text NOT NULL,
|
||||
`registerno` text,
|
||||
`title` text NOT NULL,
|
||||
`cid` text(13) NOT NULL,
|
||||
`age` integer NOT NULL,
|
||||
@@ -33,6 +35,7 @@ CREATE TABLE `users` (
|
||||
`twitter` text,
|
||||
`tiktok` text,
|
||||
`other_social` text,
|
||||
`image` text,
|
||||
`email` text,
|
||||
`job` text NOT NULL,
|
||||
`education` text NOT NULL,
|
||||
@@ -40,6 +43,7 @@ CREATE TABLE `users` (
|
||||
`reason` text,
|
||||
`group_id` integer NOT NULL,
|
||||
`zone_id` integer NOT NULL,
|
||||
`verified` integer DEFAULT false NOT NULL,
|
||||
FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`zone_id`) REFERENCES `zones`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
@@ -47,23 +51,30 @@ CREATE TABLE `users` (
|
||||
CREATE TABLE `user_opinions` (
|
||||
`user_id` integer NOT NULL,
|
||||
`opinion_id` integer NOT NULL,
|
||||
`choice` text DEFAULT 'deciding',
|
||||
`choice` text DEFAULT 'ignore',
|
||||
PRIMARY KEY(`opinion_id`, `user_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`opinion_id`) REFERENCES `opinions`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_to_selection` (
|
||||
`user_id` integer,
|
||||
`target_id` integer,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`target_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `zones` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`province_id` integer NOT NULL,
|
||||
`total` integer DEFAULT 0 NOT NULL,
|
||||
FOREIGN KEY (`province_id`) REFERENCES `provinces`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `groups_name_unique` ON `groups` (`name`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `opinions_name_unique` ON `opinions` (`name`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `provinces_name_unique` ON `provinces` (`name`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_cid_unique` ON `users` (`cid`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_phone_unique` ON `users` (`phone`);--> statement-breakpoint
|
||||
CREATE INDEX `phone_idx` ON `users` (`phone`);--> statement-breakpoint
|
||||
CREATE INDEX `image_idx` ON `users` (`image`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `zones_name_province_id_unique` ON `zones` (`name`,`province_id`);
|
||||
@@ -1,10 +0,0 @@
|
||||
CREATE TABLE `image_to_user` (
|
||||
`user_id` integer PRIMARY KEY NOT NULL,
|
||||
`image_name` text NOT NULL,
|
||||
`created_on` integer DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `phone_tokens`;--> statement-breakpoint
|
||||
ALTER TABLE users ADD `image` text;--> statement-breakpoint
|
||||
CREATE INDEX `image_idx` ON `users` (`image`);
|
||||
1
drizzle/0001_last_hiroim.sql
Normal file
1
drizzle/0001_last_hiroim.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD `rank` integer DEFAULT 9999;
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "58f80520-7300-4bc4-943d-87568666e42d",
|
||||
"id": "b46f7266-bc92-4a23-b12d-d162624db70a",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"groups": {
|
||||
@@ -35,6 +35,51 @@
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"image_to_user": {
|
||||
"name": "image_to_user",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image_name": {
|
||||
"name": "image_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_on": {
|
||||
"name": "created_on",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"image_to_user_user_id_users_id_fk": {
|
||||
"name": "image_to_user_user_id_users_id_fk",
|
||||
"tableFrom": "image_to_user",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"opinions": {
|
||||
"name": "opinions",
|
||||
"columns": {
|
||||
@@ -58,7 +103,7 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'3Choice'"
|
||||
"default": "'5Choice'"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
@@ -74,37 +119,6 @@
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"phone_tokens": {
|
||||
"name": "phone_tokens",
|
||||
"columns": {
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_on": {
|
||||
"name": "created_on",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"provinces": {
|
||||
"name": "provinces",
|
||||
"columns": {
|
||||
@@ -160,6 +174,13 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"registerno": {
|
||||
"name": "registerno",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
@@ -223,6 +244,13 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
@@ -271,29 +299,30 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"verified": {
|
||||
"name": "verified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_cid_unique": {
|
||||
"name": "users_cid_unique",
|
||||
"columns": [
|
||||
"cid"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_phone_unique": {
|
||||
"name": "users_phone_unique",
|
||||
"columns": [
|
||||
"phone"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"phone_idx": {
|
||||
"name": "phone_idx",
|
||||
"columns": [
|
||||
"phone"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"image_idx": {
|
||||
"name": "image_idx",
|
||||
"columns": [
|
||||
"image"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
@@ -350,7 +379,7 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'deciding'"
|
||||
"default": "'ignore'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -393,6 +422,56 @@
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"user_to_selection": {
|
||||
"name": "user_to_selection",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_to_selection_user_id_users_id_fk": {
|
||||
"name": "user_to_selection_user_id_users_id_fk",
|
||||
"tableFrom": "user_to_selection",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_to_selection_target_id_users_id_fk": {
|
||||
"name": "user_to_selection_target_id_users_id_fk",
|
||||
"tableFrom": "user_to_selection",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"target_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"zones": {
|
||||
"name": "zones",
|
||||
"columns": {
|
||||
@@ -416,6 +495,14 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"total": {
|
||||
"name": "total",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "2cf2acb7-cc98-4f28-8ead-5916b87b7683",
|
||||
"prevId": "58f80520-7300-4bc4-943d-87568666e42d",
|
||||
"id": "d7a1e002-36b8-4c25-b935-c826f0a2aaab",
|
||||
"prevId": "b46f7266-bc92-4a23-b12d-d162624db70a",
|
||||
"tables": {
|
||||
"groups": {
|
||||
"name": "groups",
|
||||
@@ -103,7 +103,7 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'3Choice'"
|
||||
"default": "'5Choice'"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
@@ -174,6 +174,13 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"registerno": {
|
||||
"name": "registerno",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
@@ -292,23 +299,25 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"verified": {
|
||||
"name": "verified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"rank": {
|
||||
"name": "rank",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 9999
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_cid_unique": {
|
||||
"name": "users_cid_unique",
|
||||
"columns": [
|
||||
"cid"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_phone_unique": {
|
||||
"name": "users_phone_unique",
|
||||
"columns": [
|
||||
"phone"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"phone_idx": {
|
||||
"name": "phone_idx",
|
||||
"columns": [
|
||||
@@ -378,7 +387,7 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'deciding'"
|
||||
"default": "'ignore'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -421,6 +430,56 @@
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"user_to_selection": {
|
||||
"name": "user_to_selection",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_to_selection_user_id_users_id_fk": {
|
||||
"name": "user_to_selection_user_id_users_id_fk",
|
||||
"tableFrom": "user_to_selection",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_to_selection_target_id_users_id_fk": {
|
||||
"name": "user_to_selection_target_id_users_id_fk",
|
||||
"tableFrom": "user_to_selection",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"target_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"zones": {
|
||||
"name": "zones",
|
||||
"columns": {
|
||||
@@ -444,6 +503,14 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"total": {
|
||||
"name": "total",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1713548458041,
|
||||
"tag": "0000_right_nebula",
|
||||
"when": 1717814164478,
|
||||
"tag": "0000_tired_impossible_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1713599233997,
|
||||
"tag": "0001_chilly_bullseye",
|
||||
"when": 1717817049392,
|
||||
"tag": "0001_last_hiroim",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1713537308,
|
||||
"narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
23
flake.nix
Normal file
23
flake.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
devShells.default = mkShell {
|
||||
packages = with pkgs; [
|
||||
act
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
148
initialData.ts
148
initialData.ts
@@ -1,23 +1,23 @@
|
||||
export const Groups = [
|
||||
"กลุ่มบริหารราชการแผ่นดินและความมั่นคง",
|
||||
"กลุ่มกฎหมายและกระบวนการยุติธรรม",
|
||||
"กลุ่มการศึกษา",
|
||||
"กลุ่มสาธารณสุข",
|
||||
"กลุ่มทำนา ทำไร่",
|
||||
"กลุ่มทำสวน ป่าไม้ ประมง เลี้ยงสัตว์",
|
||||
"กลุ่มลูกจ้าง ผู้ใช้แรงงาน",
|
||||
"กลุ่มผู้ประกอบกิจการ SMEs",
|
||||
"กลุ่มผู้ประกอบกิจการอื่น",
|
||||
"กลุ่มผู้ประกอบอุตสาหกรรม",
|
||||
"กลุ่มสิ่งแวดล้อม อสังหาริมทรัพย์ พลังงาน",
|
||||
"กลุ่มท่องเที่ยว โรงแรม",
|
||||
"กลุ่มวิทยาศาสตร์ เทคโนโลยี",
|
||||
"กลุ่มศิลปะ ดนตรี บันเทิง กีฬา",
|
||||
"กลุ่มประชาสังคม",
|
||||
"กลุ่มสื่อสารมวลชน นักเขียน",
|
||||
"กลุ่มอาชีพอิสระ",
|
||||
"กลุ่มการบริหารราชการแผ่นดินและความมั่นคง อันได้แก่ ผู้เคยเป็นข้าราชการ เจ้าหน้าที่ของรัฐ หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มกฎหมายและกระบวนการยุติธรรม อันได้แก่ ผู้เป็นหรือเคยเป็นผู้พิพากษา ตุลาการ อัยการ ตำรวจ ผู้ประกอบวิชาชีพด้านกฎหมาย หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มการศึกษา อันได้แก่ ผู้เป็นหรือเคยเป็นครู อาจารย์ นักวิจัย ผู้บริหารสถานศึกษา บุคลากรทางการศึกษา หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มการสาธารณสุข อันได้แก่ ผู้เป็นหรือเคยเป็นแพทย์ทุกประเภท เทคนิคการแพทย์ สาธารณสุข พยาบาล เภสัชกร หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มอาชีพทำนา ปลูกพืชล้มลุก หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มอาชีพทำสวน ป่าไม้ ปศุสัตว์ ประมง หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มพนักงานหรือลูกจ้างของบุคคลซึ่งมิใช่ส่วนราชการหรือหน่วยงานของรัฐ ผู้ใช้แรงงาน หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มผู้ประกอบอาชีพด้านสิ่งแวดล้อม ผังเมือง อสังหาริมทรัพย์และสาธารณูปโภค ทรัพยากรธรรมชาติ พลังงาน หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มผู้ประกอบกิจการขนาดกลางและขนาดย่อมตามกฎหมายว่าด้วยการนั้น หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มผู้ประกอบกิจการอื่นนอกจากกิจการตาม (๙)",
|
||||
"กลุ่มผู้ประกอบธุรกิจหรืออาชีพด้านการท่องเที่ยว อันได้แก่ ผู้ประกอบธุรกิจท่องเที่ยว มัคคุเทศก์ ผู้ประกอบกิจการหรือพนักงานโรงแรม หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มผู้ประกอบอุตสาหกรรม หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มผู้ประกอบอาชีพด้านวิทยาศาสตร์ เทคโนโลยี การสื่อสาร การพัฒนานวัตกรรม หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มสตรี",
|
||||
"กลุ่มผู้สูงอายุ คนพิการ ชาติพันธุ์ กลุ่มอัตลักษณ์อื่น",
|
||||
"กลุ่มผู้สูงอายุ คนพิการหรือทุพพลภาพ กลุ่มชาติพันธุ์ กลุ่มอัตลักษณ์อื่น หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มศิลปะ วัฒนธรรม ดนตรี การแสดงและบันเทิง นักกีฬา หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มประชาสังคม กลุ่มองค์กรสาธารณประโยชน์ หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มสื่อสารมวลชน ผู้สร้างสรรค์วรรณกรรม หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มผู้ประกอบวิชาชีพ ผู้ประกอบอาชีพอิสระ หรืออื่น ๆ ในทำนองเดียวกัน",
|
||||
"กลุ่มอื่นๆ",
|
||||
];
|
||||
|
||||
@@ -233,361 +233,361 @@ export const Provinces = [
|
||||
},
|
||||
{
|
||||
code: 38,
|
||||
name_th: "บึงกาฬ",
|
||||
name_th_short: "นธ",
|
||||
name_en: "buogkan",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 39,
|
||||
name_th: "หนองบัวลำภู",
|
||||
name_th_short: "บก",
|
||||
name_en: "Nong Bua Lam Phu",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 39,
|
||||
code: 40,
|
||||
name_th: "ขอนแก่น",
|
||||
name_th_short: "นภ",
|
||||
name_en: "Khon Kaen",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 40,
|
||||
code: 41,
|
||||
name_th: "อุดรธานี",
|
||||
name_th_short: "ขก",
|
||||
name_en: "Udon Thani",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 41,
|
||||
code: 42,
|
||||
name_th: "เลย",
|
||||
name_th_short: "อธ",
|
||||
name_en: "Loei",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 42,
|
||||
code: 43,
|
||||
name_th: "หนองคาย",
|
||||
name_th_short: "เลย",
|
||||
name_en: "Nong Khai",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 43,
|
||||
code: 44,
|
||||
name_th: "มหาสารคาม",
|
||||
name_th_short: "นค",
|
||||
name_en: "Maha Sarakham",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 44,
|
||||
code: 45,
|
||||
name_th: "ร้อยเอ็ด",
|
||||
name_th_short: "มค",
|
||||
name_en: "Roi Et",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 45,
|
||||
code: 46,
|
||||
name_th: "กาฬสินธุ์",
|
||||
name_th_short: "รอ",
|
||||
name_en: "Kalasin",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 46,
|
||||
code: 47,
|
||||
name_th: "สกลนคร",
|
||||
name_th_short: "กส",
|
||||
name_en: "Sakon Nakhon",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 47,
|
||||
code: 48,
|
||||
name_th: "นครพนม",
|
||||
name_th_short: "สน",
|
||||
name_en: "Nakhon Phanom",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 48,
|
||||
code: 49,
|
||||
name_th: "มุกดาหาร",
|
||||
name_th_short: "นพ",
|
||||
name_en: "Mukdahan",
|
||||
geography_id: 3,
|
||||
},
|
||||
{
|
||||
code: 49,
|
||||
code: 50,
|
||||
name_th: "เชียงใหม่",
|
||||
name_th_short: "มห",
|
||||
name_en: "Chiang Mai",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 50,
|
||||
code: 51,
|
||||
name_th: "ลำพูน",
|
||||
name_th_short: "ชม",
|
||||
name_en: "Lamphun",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 51,
|
||||
code: 52,
|
||||
name_th: "ลำปาง",
|
||||
name_th_short: "ลพ",
|
||||
name_en: "Lampang",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 52,
|
||||
code: 53,
|
||||
name_th: "อุตรดิตถ์",
|
||||
name_th_short: "ลป",
|
||||
name_en: "Uttaradit",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 53,
|
||||
code: 54,
|
||||
name_th: "แพร่",
|
||||
name_th_short: "อด",
|
||||
name_en: "Phrae",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 54,
|
||||
code: 55,
|
||||
name_th: "น่าน",
|
||||
name_th_short: "พร",
|
||||
name_en: "Nan",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 55,
|
||||
code: 56,
|
||||
name_th: "พะเยา",
|
||||
name_th_short: "นน",
|
||||
name_en: "Phayao",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 56,
|
||||
code: 57,
|
||||
name_th: "เชียงราย",
|
||||
name_th_short: "พย",
|
||||
name_en: "Chiang Rai",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 57,
|
||||
code: 58,
|
||||
name_th: "แม่ฮ่องสอน",
|
||||
name_th_short: "ชร",
|
||||
name_en: "Mae Hong Son",
|
||||
geography_id: 1,
|
||||
},
|
||||
{
|
||||
code: 58,
|
||||
code: 60,
|
||||
name_th: "นครสวรรค์",
|
||||
name_th_short: "มส",
|
||||
name_en: "Nakhon Sawan",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 60,
|
||||
code: 61,
|
||||
name_th: "อุทัยธานี",
|
||||
name_th_short: "นว",
|
||||
name_en: "Uthai Thani",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 61,
|
||||
code: 62,
|
||||
name_th: "กำแพงเพชร",
|
||||
name_th_short: "อน",
|
||||
name_en: "Kamphaeng Phet",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 62,
|
||||
code: 63,
|
||||
name_th: "ตาก",
|
||||
name_th_short: "กพ",
|
||||
name_en: "Tak",
|
||||
geography_id: 4,
|
||||
},
|
||||
{
|
||||
code: 63,
|
||||
code: 64,
|
||||
name_th: "สุโขทัย",
|
||||
name_th_short: "ตก",
|
||||
name_en: "Sukhothai",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 64,
|
||||
code: 65,
|
||||
name_th: "พิษณุโลก",
|
||||
name_th_short: "สท",
|
||||
name_en: "Phitsanulok",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 65,
|
||||
code: 66,
|
||||
name_th: "พิจิตร",
|
||||
name_th_short: "พล",
|
||||
name_en: "Phichit",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 66,
|
||||
code: 67,
|
||||
name_th: "เพชรบูรณ์",
|
||||
name_th_short: "พจ",
|
||||
name_en: "Phetchabun",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 67,
|
||||
code: 70,
|
||||
name_th: "ราชบุรี",
|
||||
name_th_short: "พช",
|
||||
name_en: "Ratchaburi",
|
||||
geography_id: 4,
|
||||
},
|
||||
{
|
||||
code: 70,
|
||||
code: 71,
|
||||
name_th: "กาญจนบุรี",
|
||||
name_th_short: "รบ",
|
||||
name_en: "Kanchanaburi",
|
||||
geography_id: 4,
|
||||
},
|
||||
{
|
||||
code: 71,
|
||||
code: 72,
|
||||
name_th: "สุพรรณบุรี",
|
||||
name_th_short: "กจ",
|
||||
name_en: "Suphan Buri",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 72,
|
||||
code: 73,
|
||||
name_th: "นครปฐม",
|
||||
name_th_short: "สพ",
|
||||
name_en: "Nakhon Pathom",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 73,
|
||||
code: 74,
|
||||
name_th: "สมุทรสาคร",
|
||||
name_th_short: "นป",
|
||||
name_en: "Samut Sakhon",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 74,
|
||||
code: 75,
|
||||
name_th: "สมุทรสงคราม",
|
||||
name_th_short: "สค",
|
||||
name_en: "Samut Songkhram",
|
||||
geography_id: 2,
|
||||
},
|
||||
{
|
||||
code: 75,
|
||||
code: 76,
|
||||
name_th: "เพชรบุรี",
|
||||
name_th_short: "สส",
|
||||
name_en: "Phetchaburi",
|
||||
geography_id: 4,
|
||||
},
|
||||
{
|
||||
code: 76,
|
||||
code: 77,
|
||||
name_th: "ประจวบคีรีขันธ์",
|
||||
name_th_short: "พบ",
|
||||
name_en: "Prachuap Khiri Khan",
|
||||
geography_id: 4,
|
||||
},
|
||||
{
|
||||
code: 77,
|
||||
code: 80,
|
||||
name_th: "นครศรีธรรมราช",
|
||||
name_th_short: "ปข",
|
||||
name_en: "Nakhon Si Thammarat",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 80,
|
||||
code: 81,
|
||||
name_th: "กระบี่",
|
||||
name_th_short: "นศ",
|
||||
name_en: "Krabi",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 81,
|
||||
code: 82,
|
||||
name_th: "พังงา",
|
||||
name_th_short: "กบ",
|
||||
name_en: "Phangnga",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 82,
|
||||
code: 83,
|
||||
name_th: "ภูเก็ต",
|
||||
name_th_short: "พง",
|
||||
name_en: "Phuket",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 83,
|
||||
code: 84,
|
||||
name_th: "สุราษฎร์ธานี",
|
||||
name_th_short: "ภก",
|
||||
name_en: "Surat Thani",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 84,
|
||||
code: 85,
|
||||
name_th: "ระนอง",
|
||||
name_th_short: "สฎ",
|
||||
name_en: "Ranong",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 85,
|
||||
code: 86,
|
||||
name_th: "ชุมพร",
|
||||
name_th_short: "รน",
|
||||
name_en: "Chumphon",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 86,
|
||||
code: 90,
|
||||
name_th: "สงขลา",
|
||||
name_th_short: "ชพ",
|
||||
name_en: "Songkhla",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 90,
|
||||
code: 91,
|
||||
name_th: "สตูล",
|
||||
name_th_short: "สข",
|
||||
name_en: "Satun",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 91,
|
||||
code: 92,
|
||||
name_th: "ตรัง",
|
||||
name_th_short: "สต",
|
||||
name_en: "Trang",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 92,
|
||||
code: 93,
|
||||
name_th: "พัทลุง",
|
||||
name_th_short: "ตง",
|
||||
name_en: "Phatthalung",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 93,
|
||||
code: 94,
|
||||
name_th: "ปัตตานี",
|
||||
name_th_short: "พท",
|
||||
name_en: "Pattani",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 94,
|
||||
code: 95,
|
||||
name_th: "ยะลา",
|
||||
name_th_short: "ปน",
|
||||
name_en: "Yala",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 95,
|
||||
code: 96,
|
||||
name_th: "นราธิวาส",
|
||||
name_th_short: "ยล",
|
||||
name_en: "Narathiwat",
|
||||
geography_id: 6,
|
||||
},
|
||||
{
|
||||
code: 96,
|
||||
name_th: "บึงกาฬ",
|
||||
name_th_short: "นธ",
|
||||
name_en: "buogkan",
|
||||
geography_id: 3,
|
||||
},
|
||||
];
|
||||
export const Districts = [
|
||||
{
|
||||
|
||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
68
next.Dockerfile
Normal file
68
next.Dockerfile
Normal file
@@ -0,0 +1,68 @@
|
||||
FROM node:18-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* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable 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 \
|
||||
if [ -f yarn.lock ]; then yarn run next-build; \
|
||||
elif [ -f package-lock.json ]; then npm run next-build; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run next-build; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
# 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 #Only needed if public is there
|
||||
|
||||
# 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
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||
CMD HOSTNAME="0.0.0.0" node server.js
|
||||
3
next.config.js
Normal file
3
next.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
output: "standalone",
|
||||
};
|
||||
20
package.json
20
package.json
@@ -6,8 +6,11 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "nodemon --exec ts-node --swc src/app.ts",
|
||||
"next-dev": "next dev",
|
||||
"next-build": "next build",
|
||||
"start": "node dist/src/app.js",
|
||||
"build": "swc src -d dist",
|
||||
"lint": "next lint",
|
||||
"initialize_data": "node -r @swc-node/register addMetadata.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -16,6 +19,7 @@
|
||||
"dependencies": {
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@types/react": "^18.3.2",
|
||||
"better-sqlite3": "^9.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"drizzle-orm": "^0.30.8",
|
||||
@@ -23,10 +27,14 @@
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"minio": "^7.1.3",
|
||||
"next": "^14.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"trpc-playground": "^1.0.4",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.2.0",
|
||||
"@swc-node/register": "^1.9.0",
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "^1.4.16",
|
||||
@@ -34,9 +42,19 @@
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"drizzle-kit": "^0.20.14",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"nodemon": "^3.1.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
2660
pnpm-lock.yaml
generated
2660
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
6
prettier.config.mjs
Normal file
6
prettier.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
92
src/adminRoute.ts
Normal file
92
src/adminRoute.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { router, publicProcedure } from "./trpc";
|
||||
import { db } from "./db";
|
||||
import { user, userOpinion } from "./schema";
|
||||
import { count, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
export const adminRoute = router({
|
||||
totalUser: publicProcedure.query(async () => {
|
||||
const rs = await db
|
||||
.select({ zone: user.zone, value: count(user.id) })
|
||||
.from(user)
|
||||
.groupBy(user.zone)
|
||||
.execute();
|
||||
const zones = await db.query.zone
|
||||
.findMany({ with: { province: true } })
|
||||
.execute();
|
||||
zones.sort((a, b) => a.province.id - b.province.id);
|
||||
const summary = zones.map((z) => {
|
||||
const num = rs.find((user) => user.zone == z.id)?.value ?? 0;
|
||||
return {
|
||||
count: num,
|
||||
zone: z.name,
|
||||
province: z.province.name,
|
||||
};
|
||||
});
|
||||
return summary;
|
||||
}),
|
||||
totalUserDeep: publicProcedure
|
||||
.input(z.object({ code: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
const users = await db
|
||||
.select({
|
||||
zone: user.zone,
|
||||
cid: user.cid,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
phone: user.phone,
|
||||
})
|
||||
.from(user)
|
||||
.execute();
|
||||
const zones = await db.query.zone
|
||||
.findMany({ with: { province: true } })
|
||||
.execute();
|
||||
zones.sort((a, b) => a.province.id - b.province.id);
|
||||
let rs = [];
|
||||
for (const zone of zones) {
|
||||
const zoneUser = users.filter((u) => u.zone == zone.id);
|
||||
if (zoneUser.length == 0) continue;
|
||||
const total = zoneUser.length;
|
||||
let userDescription: string;
|
||||
if (input.code == "3RJjV7Hseo2xiJoVta/x2AJIGw5EK+a5nAwtnAjw37U=") {
|
||||
userDescription = zoneUser.reduce(
|
||||
(acc, n) =>
|
||||
acc + `${n.firstName} ${n.lastName}: ${n.cid}|${n.phone}\n`,
|
||||
"",
|
||||
);
|
||||
} else {
|
||||
userDescription = zoneUser.reduce(
|
||||
(acc, n) => acc + `${n.firstName} ${n.lastName}: ${n.cid}\n`,
|
||||
"",
|
||||
);
|
||||
}
|
||||
rs.push({
|
||||
province: zone.province.name,
|
||||
zone: zone.name,
|
||||
total,
|
||||
users: userDescription,
|
||||
});
|
||||
}
|
||||
return rs;
|
||||
}),
|
||||
|
||||
removeUser: publicProcedure
|
||||
.input(z.object({ cid: z.string(), key: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
if (input.key !== "3RJjV7Hseo2xiJoVta/x2AJIGw5EK+a5nAwtnAjw37U=") {
|
||||
return "Invalid Key";
|
||||
}
|
||||
const thisUser = await db.query.user
|
||||
.findFirst({ where: eq(user.cid, input.cid) })
|
||||
.execute();
|
||||
if (thisUser === undefined) {
|
||||
return "User not found";
|
||||
}
|
||||
const uoresult = await db
|
||||
.delete(userOpinion)
|
||||
.where(eq(userOpinion.userId, thisUser.id))
|
||||
.execute();
|
||||
const rs = await db.delete(user).where(eq(user.cid, input.cid)).execute();
|
||||
return { useropinion: uoresult, rs };
|
||||
}),
|
||||
});
|
||||
@@ -4,10 +4,12 @@ import { userRoute } from "./userRoute";
|
||||
import { runPlayground } from "./playgroud";
|
||||
import cors from "cors";
|
||||
import { infoRoute } from "./infoRoute";
|
||||
import { adminRoute } from "./adminRoute";
|
||||
|
||||
export const appRouter = router({
|
||||
user: userRoute,
|
||||
info: infoRoute,
|
||||
OjTBXE4m1xAULqhbxj3yiQ: adminRoute,
|
||||
});
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
@@ -15,7 +17,10 @@ async function main() {
|
||||
const server = createHTTPServer({
|
||||
createContext: createContext,
|
||||
router: appRouter,
|
||||
middleware: cors(),
|
||||
middleware:
|
||||
(process.env.NODE_ENV ?? "development") == "production"
|
||||
? undefined
|
||||
: cors(),
|
||||
});
|
||||
|
||||
server.listen(3000);
|
||||
|
||||
@@ -4,5 +4,6 @@ import Database from "better-sqlite3";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const sqlite = new Database("sqlite.db");
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
migrate(db, { migrationsFolder: "drizzle" });
|
||||
|
||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
||||
export const infoRoute = router({
|
||||
getAllProvinces: publicProcedure.query(getProvinces),
|
||||
getAllGroups: publicProcedure.query(getGroups),
|
||||
getAllOpinions: publicProcedure.query(getOpinions),
|
||||
getAllZones: publicProcedure
|
||||
.input(z.object({ provice_id: z.number().optional() }))
|
||||
.query(async ({ input }) => await getZone(input.provice_id)),
|
||||
@@ -28,3 +29,7 @@ async function getZone(province?: number) {
|
||||
async function getGroups() {
|
||||
return await db.query.group.findMany();
|
||||
}
|
||||
|
||||
async function getOpinions() {
|
||||
return await db.query.opinion.findMany();
|
||||
}
|
||||
|
||||
@@ -16,18 +16,18 @@ export function createClient() {
|
||||
export async function createUploadImageUrl(
|
||||
mc: minio.Client,
|
||||
objectName: string,
|
||||
contentType: string
|
||||
contentType: string,
|
||||
) {
|
||||
let policy = mc.newPostPolicy();
|
||||
const policy = mc.newPostPolicy();
|
||||
policy.setKey(objectName);
|
||||
policy.setBucket(Config.bucketName);
|
||||
let expires = new Date();
|
||||
const expires = new Date();
|
||||
expires.setSeconds(30 * 60);
|
||||
policy.setExpires(expires);
|
||||
policy.setContentType(contentType);
|
||||
policy.setContentDisposition(`attachment; filename="${objectName}"`);
|
||||
policy.setContentLengthRange(1, 3 * 1024 * 1024);
|
||||
let rs = await mc.presignedPostPolicy(policy);
|
||||
const rs = await mc.presignedPostPolicy(policy);
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const runPlayground = async (appRouter: AppRouter) => {
|
||||
trpcApiEndpoint,
|
||||
playgroundEndpoint,
|
||||
router: appRouter,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
app.listen(3001, () => {
|
||||
|
||||
@@ -15,10 +15,11 @@ export const user = sqliteTable(
|
||||
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
|
||||
firstName: text("firstName").notNull(),
|
||||
lastName: text("lastName").notNull(),
|
||||
registerno: text("registerno"),
|
||||
title: text("title").notNull(),
|
||||
cid: text("cid", { length: 13 }).notNull().unique(),
|
||||
cid: text("cid", { length: 13 }).notNull(),
|
||||
age: integer("age").notNull(),
|
||||
phone: text("phone").unique().notNull(),
|
||||
phone: text("phone").notNull(),
|
||||
public_phone: text("public_phone"),
|
||||
facebook: text("facebook"),
|
||||
twitter: text("twitter"),
|
||||
@@ -36,11 +37,13 @@ export const user = sqliteTable(
|
||||
zone: integer("zone_id")
|
||||
.notNull()
|
||||
.references(() => zone.id),
|
||||
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||
rank: integer("rank").default(9999),
|
||||
},
|
||||
(t) => ({
|
||||
phone_idx: index("phone_idx").on(t.phone),
|
||||
image_idx: index("image_idx").on(t.image),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
export const userRelation = relations(user, ({ many, one }) => ({
|
||||
@@ -53,6 +56,7 @@ export const userRelation = relations(user, ({ many, one }) => ({
|
||||
fields: [user.zone],
|
||||
references: [zone.id],
|
||||
}),
|
||||
userToSelection: many(userToSelection, { relationName: "userRelation" }),
|
||||
}));
|
||||
|
||||
//----------------Group
|
||||
@@ -69,8 +73,8 @@ export const groupRelation = relations(group, ({ many }) => ({
|
||||
export const opinion = sqliteTable("opinions", {
|
||||
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
|
||||
name: text("name").unique().notNull(),
|
||||
type: text("type", { enum: ["3Choice", "4Choice"] })
|
||||
.default("3Choice")
|
||||
type: text("type", { enum: ["3Choice", "4Choice", "5Choice"] })
|
||||
.default("5Choice")
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
@@ -85,12 +89,19 @@ export const userOpinion = sqliteTable(
|
||||
.notNull()
|
||||
.references(() => opinion.id),
|
||||
choice: text("choice", {
|
||||
enum: ["agree", "disagree", "deciding", "ignore"],
|
||||
}).default("deciding"),
|
||||
enum: [
|
||||
"strongly agree",
|
||||
"agree",
|
||||
"disagree",
|
||||
"strongly disagree",
|
||||
"ignore",
|
||||
"deciding",
|
||||
],
|
||||
}).default("ignore"),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.userId, t.opinionId] }),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
export const userOpinionRelation = relations(userOpinion, ({ one }) => ({
|
||||
@@ -109,8 +120,9 @@ export const zone = sqliteTable(
|
||||
province: integer("province_id")
|
||||
.notNull()
|
||||
.references(() => province.id),
|
||||
total: integer("total").notNull().default(0),
|
||||
},
|
||||
(t) => ({ unique_name_province: unique().on(t.name, t.province) })
|
||||
(t) => ({ unique_name_province: unique().on(t.name, t.province) }),
|
||||
);
|
||||
export const zoneRelation = relations(zone, ({ one }) => ({
|
||||
province: one(province, {
|
||||
@@ -136,6 +148,29 @@ export const imageToUser = sqliteTable("image_to_user", {
|
||||
.references(() => user.id),
|
||||
imageName: text("image_name").notNull(),
|
||||
createdOn: integer("created_on", { mode: "timestamp" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
sql`CURRENT_TIMESTAMP`,
|
||||
),
|
||||
});
|
||||
|
||||
export const userToSelection = sqliteTable("user_to_selection", {
|
||||
userId: integer("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||
targetId: integer("target_id").references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const userToSelectionRelation = relations(
|
||||
userToSelection,
|
||||
({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [userToSelection.userId],
|
||||
references: [user.id],
|
||||
relationName: "userRelation",
|
||||
}),
|
||||
selection: one(user, {
|
||||
fields: [userToSelection.targetId],
|
||||
references: [user.id],
|
||||
relationName: "selectionRelation",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ export const createContext = async (opts: CreateHTTPContextOptions) => {
|
||||
const bearerToken = authorizationHeader.split(" ")[1];
|
||||
const phone = await verifyToken(bearerToken);
|
||||
if (phone !== null) {
|
||||
let user = await db.query.user.findFirst({
|
||||
const user = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.phone, phone),
|
||||
});
|
||||
return {
|
||||
@@ -59,7 +59,7 @@ export const createContext = async (opts: CreateHTTPContextOptions) => {
|
||||
|
||||
async function verifyToken(token: string): Promise<string | null> {
|
||||
try {
|
||||
let rs = await new Promise((resolve, reject) => {
|
||||
const rs = await new Promise((resolve, reject) => {
|
||||
jwt.verify(token, Config.jwt_secret, (err, decoded) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
@@ -68,7 +68,7 @@ async function verifyToken(token: string): Promise<string | null> {
|
||||
}
|
||||
});
|
||||
});
|
||||
let data = z
|
||||
const data = z
|
||||
.object({
|
||||
phone: z.string(),
|
||||
})
|
||||
|
||||
255
src/userRoute.ts
255
src/userRoute.ts
@@ -1,9 +1,9 @@
|
||||
import { router, publicProcedure, protectedProcedure } from "./trpc";
|
||||
import { db } from "./db";
|
||||
import { imageToUser, opinion, user, userOpinion } from "./schema";
|
||||
import { imageToUser, opinion, user, userOpinion, zone } from "./schema";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { SQL, count, eq } from "drizzle-orm";
|
||||
import { SQL, and, count, eq, inArray, sql } from "drizzle-orm";
|
||||
import { Config } from "./config";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
@@ -12,7 +12,7 @@ import { createClient, createUploadImageUrl } from "./minio";
|
||||
const userInsertSchema = createInsertSchema(user, {
|
||||
cid: (schema) =>
|
||||
schema.cid.length(13).refine(isValidThaiID, { message: "Invalid Thai ID" }),
|
||||
});
|
||||
}).omit({ verified: true });
|
||||
|
||||
const userUpdateSchema = userInsertSchema
|
||||
.omit({ id: true, cid: true, phone: true })
|
||||
@@ -27,8 +27,10 @@ const opinionInsertSchema = createInsertSchema(userOpinion)
|
||||
const opinionUpdateSchema = createInsertSchema(userOpinion)
|
||||
.omit({
|
||||
userId: true,
|
||||
verified: true,
|
||||
})
|
||||
.required({ opinionId: true });
|
||||
|
||||
type OpinionInsertSchema = z.infer<typeof opinionInsertSchema>;
|
||||
type UserInsertSchema = z.infer<typeof userInsertSchema>;
|
||||
type UserUpdateSchema = z.infer<typeof userUpdateSchema>;
|
||||
@@ -38,15 +40,21 @@ export const userRoute = router({
|
||||
.input(
|
||||
userInsertSchema.omit({ id: true }).extend({
|
||||
opinions: opinionInsertSchema,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(
|
||||
async ({ input }) => await createUser({ ...input }, input.opinions)
|
||||
async ({ input }) => await createUser({ ...input }, input.opinions),
|
||||
),
|
||||
// changeImage: protectedProcedure
|
||||
updateUser: protectedProcedure
|
||||
.input(userUpdateSchema)
|
||||
.mutation(async ({ input, ctx }) => await updateUser(ctx.user.id, input)),
|
||||
getUser: publicProcedure
|
||||
.input(z.object({ userId: z.number() }))
|
||||
.mutation(async ({ input }) => await getUser(input.userId, false)),
|
||||
getSelf: protectedProcedure.mutation(
|
||||
async ({ ctx }) => await getUser(ctx.user.id, true),
|
||||
),
|
||||
login: publicProcedure
|
||||
.input(z.object({ cid: z.string(), phone: z.string() }))
|
||||
.mutation(async ({ input }) => await login(input.cid, input.phone)),
|
||||
@@ -54,7 +62,7 @@ export const userRoute = router({
|
||||
.input(opinionUpdateSchema)
|
||||
.mutation(
|
||||
async ({ input, ctx }) =>
|
||||
await changeOpinion(input.opinionId, ctx.user.id, input.choice)
|
||||
await changeOpinion(input.opinionId, ctx.user.id, input.choice),
|
||||
),
|
||||
requestChangeImage: protectedProcedure
|
||||
.input(z.object({ imageName: z.string(), contentType: z.string() }))
|
||||
@@ -63,21 +71,23 @@ export const userRoute = router({
|
||||
await requestChangeImage(
|
||||
ctx.user.id,
|
||||
input.imageName,
|
||||
input.contentType
|
||||
)
|
||||
input.contentType,
|
||||
),
|
||||
),
|
||||
confirmChangeImage: protectedProcedure.mutation(
|
||||
async ({ ctx }) => await confirmChangeImage(ctx.user.id, ctx.user.image)
|
||||
async ({ ctx }) => await confirmChangeImage(ctx.user.id, ctx.user.image),
|
||||
),
|
||||
getAllUser: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
offset: z.number().default(0),
|
||||
limit: z.number().max(50).default(10),
|
||||
limit: z.number().max(1000).default(1000),
|
||||
group: z.number().optional(),
|
||||
zone: z.number().optional(),
|
||||
opinionCount: z.number().default(3),
|
||||
})
|
||||
province: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.query(
|
||||
async ({ input }) =>
|
||||
@@ -86,18 +96,113 @@ export const userRoute = router({
|
||||
input.limit,
|
||||
input.opinionCount,
|
||||
input.group,
|
||||
input.zone
|
||||
)
|
||||
input.zone,
|
||||
input.province,
|
||||
input.userId,
|
||||
),
|
||||
),
|
||||
getAllUserCount: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
group: z.number().optional(),
|
||||
zone: z.number().optional(),
|
||||
province: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.query(
|
||||
async ({ input }) =>
|
||||
await getAllUserCount(input.group, input.zone, input.province),
|
||||
),
|
||||
});
|
||||
|
||||
async function getAllUserCount(
|
||||
group?: number,
|
||||
zoneId?: number,
|
||||
provinceId?: number,
|
||||
) {
|
||||
const zoneIds: number[] = await getZone(provinceId);
|
||||
if (provinceId && zoneIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const conditions: SQL[] = [];
|
||||
if (group !== undefined) {
|
||||
conditions.push(eq(user.group, group));
|
||||
}
|
||||
if (zoneId !== undefined) {
|
||||
conditions.push(eq(user.zone, zoneId));
|
||||
}
|
||||
if (zoneIds.length > 0) {
|
||||
conditions.push(inArray(user.zone, zoneIds));
|
||||
}
|
||||
return await db
|
||||
.select({ count: count(user.id) })
|
||||
.from(user)
|
||||
.where(and(...conditions))
|
||||
.then((v) => v[0].count);
|
||||
}
|
||||
|
||||
async function getAllUser(
|
||||
offset: number,
|
||||
limit: number,
|
||||
opinionLimit: number,
|
||||
group?: number,
|
||||
zone?: number
|
||||
zoneId?: number,
|
||||
provinceId?: number,
|
||||
userId?: number,
|
||||
) {
|
||||
const zoneIds: number[] = await getZone(provinceId);
|
||||
if (provinceId && zoneIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let thisUser =
|
||||
userId == undefined
|
||||
? undefined
|
||||
: await db.query.user.findFirst({
|
||||
where: eq(user.id, userId),
|
||||
with: {
|
||||
userToSelection: {
|
||||
with: {
|
||||
selection: {
|
||||
with: {
|
||||
group: true,
|
||||
opinions: {
|
||||
limit: opinionLimit,
|
||||
},
|
||||
zone: {
|
||||
with: { province: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const topTen = await db.query.user.findMany({
|
||||
with: {
|
||||
group: true,
|
||||
opinions: {
|
||||
limit: opinionLimit,
|
||||
},
|
||||
zone: {
|
||||
with: { province: true },
|
||||
},
|
||||
},
|
||||
limit: 10,
|
||||
orderBy: user.rank,
|
||||
where: (user, { eq, and }) => {
|
||||
const conditions: SQL[] = [];
|
||||
if (group !== undefined) {
|
||||
conditions.push(eq(user.group, group));
|
||||
}
|
||||
if (zoneId !== undefined) {
|
||||
conditions.push(eq(user.zone, zoneId));
|
||||
}
|
||||
if (zoneIds.length > 0) {
|
||||
conditions.push(inArray(user.zone, zoneIds));
|
||||
}
|
||||
return and(...conditions);
|
||||
},
|
||||
});
|
||||
let users = await db.query.user.findMany({
|
||||
with: {
|
||||
group: true,
|
||||
@@ -110,34 +215,90 @@ async function getAllUser(
|
||||
},
|
||||
limit,
|
||||
offset,
|
||||
orderBy: sql`random()`,
|
||||
where: (user, { eq, and }) => {
|
||||
let conditions: SQL[] = [];
|
||||
if (group) {
|
||||
const conditions: SQL[] = [];
|
||||
if (group !== undefined) {
|
||||
conditions.push(eq(user.group, group));
|
||||
}
|
||||
if (zone) {
|
||||
conditions.push(eq(user.zone, zone));
|
||||
if (zoneId !== undefined) {
|
||||
conditions.push(eq(user.zone, zoneId));
|
||||
}
|
||||
if (zoneIds.length > 0) {
|
||||
conditions.push(inArray(user.zone, zoneIds));
|
||||
}
|
||||
return and(...conditions);
|
||||
},
|
||||
});
|
||||
let resultUser: typeof users;
|
||||
if (thisUser && thisUser.group == group) {
|
||||
const selections = thisUser.userToSelection.map((v) => v.selection);
|
||||
let validSelection: typeof users = [];
|
||||
for (const sl of selections) {
|
||||
if (sl !== null) {
|
||||
validSelection.push(sl);
|
||||
}
|
||||
}
|
||||
resultUser = [
|
||||
...validSelection,
|
||||
...users.filter(
|
||||
(u) => validSelection.filter((v) => v.id == u.id).length == 0,
|
||||
),
|
||||
];
|
||||
resultUser = randomArray(1, 10, resultUser);
|
||||
} else {
|
||||
resultUser = [
|
||||
...topTen,
|
||||
...users.filter((u) => topTen.filter((v) => v.id == u.id).length == 0),
|
||||
];
|
||||
resultUser = randomArray(0, 5, resultUser);
|
||||
resultUser = randomArray(5, 10, resultUser);
|
||||
}
|
||||
|
||||
return users.map((u) => ({
|
||||
...u,
|
||||
phone: hidePhone(u.phone),
|
||||
image: u.image ? `${Config.minioPublicBucketEndpoint}${u.image}` : null,
|
||||
}));
|
||||
return resultUser
|
||||
.map((u) => ({
|
||||
...u,
|
||||
phone: hidePhone(u.phone),
|
||||
verified: true,
|
||||
image: u.image ? `${Config.minioPublicBucketEndpoint}${u.image}` : null,
|
||||
}))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
async function getUser(userId: number, showPhone: boolean) {
|
||||
const user = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.id, userId),
|
||||
with: {
|
||||
group: true,
|
||||
opinions: true,
|
||||
zone: true,
|
||||
},
|
||||
});
|
||||
if (user === undefined) {
|
||||
throw new TRPCError({
|
||||
message: "User not found",
|
||||
code: "BAD_REQUEST",
|
||||
});
|
||||
}
|
||||
return {
|
||||
...user,
|
||||
phone: showPhone ? user.phone : hidePhone(user.phone),
|
||||
image: user.image
|
||||
? `${Config.minioPublicBucketEndpoint}${user.image}`
|
||||
: null,
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function createUser(
|
||||
newUser: UserInsertSchema,
|
||||
opinions: OpinionInsertSchema
|
||||
opinions: OpinionInsertSchema,
|
||||
) {
|
||||
try {
|
||||
let result = (
|
||||
const result = (
|
||||
await db.insert(user).values(newUser).returning({ id: user.id })
|
||||
)[0];
|
||||
for (let op of opinions) {
|
||||
for (const op of opinions) {
|
||||
await db.insert(userOpinion).values({ ...op, userId: result.id });
|
||||
}
|
||||
return { token: createJWT(newUser.phone) };
|
||||
@@ -158,7 +319,7 @@ async function updateUser(userId: number, update: UserUpdateSchema) {
|
||||
}
|
||||
|
||||
async function login(cid: string, phone: string) {
|
||||
let user = await db.query.user.findFirst({
|
||||
const user = await db.query.user.findFirst({
|
||||
where: (user, { and, eq }) => and(eq(user.cid, cid), eq(user.phone, phone)),
|
||||
});
|
||||
if (user === undefined) {
|
||||
@@ -174,10 +335,10 @@ async function login(cid: string, phone: string) {
|
||||
async function changeOpinion(
|
||||
opinionId: number,
|
||||
userId: number,
|
||||
opinionChoice: OpinionInsertSchema[0]["choice"]
|
||||
opinionChoice: OpinionInsertSchema[0]["choice"],
|
||||
) {
|
||||
try {
|
||||
let thisOpinion = await db
|
||||
const thisOpinion = await db
|
||||
.select()
|
||||
.from(opinion)
|
||||
.where(eq(opinion.id, opinionId))
|
||||
@@ -218,7 +379,7 @@ async function changeOpinion(
|
||||
async function requestChangeImage(
|
||||
userId: number,
|
||||
imageName: string,
|
||||
contentType: string
|
||||
contentType: string,
|
||||
) {
|
||||
const mc = createClient();
|
||||
// Check if the image is valid
|
||||
@@ -238,11 +399,11 @@ async function requestChangeImage(
|
||||
});
|
||||
}
|
||||
// Create a unique image name
|
||||
let tryCount = 0;
|
||||
const tryCount = 0;
|
||||
let objectName: string | null = null;
|
||||
while (tryCount < 3) {
|
||||
let imageName = `${generateRandomString()}.${extension}`;
|
||||
let ok = await db
|
||||
const imageName = `${generateRandomString()}.${extension}`;
|
||||
const ok = await db
|
||||
.select({ value: count(user.image) })
|
||||
.from(user)
|
||||
.where(eq(user.image, imageName))
|
||||
@@ -266,14 +427,14 @@ async function requestChangeImage(
|
||||
target: [imageToUser.userId],
|
||||
set: { imageName: objectName },
|
||||
});
|
||||
let request = await createUploadImageUrl(mc, objectName, contentType);
|
||||
const request = await createUploadImageUrl(mc, objectName, contentType);
|
||||
request.postURL = Config.minioPublicBucketEndpoint;
|
||||
return request;
|
||||
}
|
||||
|
||||
async function confirmChangeImage(userId: number, oldImage: string | null) {
|
||||
const mc = createClient();
|
||||
let rs = await db
|
||||
const rs = await db
|
||||
.select({ imageName: imageToUser.imageName })
|
||||
.from(imageToUser)
|
||||
.where(eq(imageToUser.userId, userId));
|
||||
@@ -283,7 +444,7 @@ async function confirmChangeImage(userId: number, oldImage: string | null) {
|
||||
code: "BAD_REQUEST",
|
||||
});
|
||||
}
|
||||
let imageName = rs[0].imageName;
|
||||
const imageName = rs[0].imageName;
|
||||
const isImageExist = await mc
|
||||
.statObject(Config.bucketName, imageName)
|
||||
.then(() => true)
|
||||
@@ -294,7 +455,7 @@ async function confirmChangeImage(userId: number, oldImage: string | null) {
|
||||
code: "BAD_REQUEST",
|
||||
});
|
||||
}
|
||||
const promises: Promise<any>[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
if (oldImage) {
|
||||
promises.push(mc.removeObject(Config.bucketName, oldImage).catch(() => {}));
|
||||
}
|
||||
@@ -335,3 +496,23 @@ function createJWT(phone: string) {
|
||||
function generateRandomString() {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
async function getZone(province?: number) {
|
||||
if (province === undefined) {
|
||||
return [];
|
||||
}
|
||||
const zoneIds = await db
|
||||
.select({ id: zone.id })
|
||||
.from(zone)
|
||||
.where(eq(zone.province, province))
|
||||
.then((queryResult) => queryResult.map((v) => v.id));
|
||||
return zoneIds;
|
||||
}
|
||||
|
||||
function randomArray<T>(from: number, to: number, arr: T[]): T[] {
|
||||
for (let i = Math.min(arr.length - 1, from); i < to; i++) {
|
||||
const j = Math.floor(Math.random() * (i - from)) + from;
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
@@ -9,10 +8,8 @@
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
@@ -23,13 +20,14 @@
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"baseUrl": "." /* Specify the base directory to resolve non-relative module names. */,
|
||||
"paths": {
|
||||
"@/components/*": ["components/*"],
|
||||
"@/src/*": ["src/*"]
|
||||
},
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
@@ -39,15 +37,13 @@
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "resolveJsonModule": true /* Enable importing .json files. */,
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
@@ -72,18 +68,13 @@
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */ /* Type Checking */,
|
||||
"strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
@@ -101,9 +92,29 @@
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next.config.js"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user