commit 33cbeaa9a3e5e970e1366017f8c43a9aad7f62ab Author: bootunloader Date: Wed Sep 11 02:57:43 2024 +0300 initial commit ?? diff --git a/.dockerignore b/.dockerignore new file mode 100755 index 0000000..a735ebe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +build +.env diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..e825215 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +build +.env +.svelte-kit diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..ea31894 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:18-alpine as builder + +RUN apk add --no-cache libc6-compat + +RUN npm i -g pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ + +RUN pnpm install + +COPY . . + +RUN pnpm run build + +EXPOSE 80 + +CMD ["pnpm", "run", "start"] diff --git a/Dockerfile.bun b/Dockerfile.bun new file mode 100755 index 0000000..115ffd5 --- /dev/null +++ b/Dockerfile.bun @@ -0,0 +1,15 @@ +FROM oven/bun:1.0.0 + +WORKDIR /app + +COPY package.json bun.lockb ./ + +RUN bun install + +COPY . . + +RUN bun run build + +EXPOSE 80 + +CMD ["bun", "run", "start"] diff --git a/components.json b/components.json new file mode 100644 index 0000000..0d5c9a5 --- /dev/null +++ b/components.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "default", + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app.css", + "baseColor": "gray" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils" + }, + "typescript": true +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100755 index 0000000..e92d4d1 --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "rdv", + "description": "", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "start": "HOST=0.0.0.0 PORT=80 node ./build/index.js", + "build": "pnpm run check && vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test:unit": "vitest --watch" + }, + "dependencies": { + "@melt-ui/svelte": "^0.22.2", + "@sveltejs/svelte-virtual-list": "^3.0.1", + "@tanstack/match-sorter-utils": "^8.8.4", + "@tanstack/svelte-query": "^4.35.3", + "@tanstack/svelte-table": "^8.9.4", + "@trpc/client": "^10.45.2", + "@trpc/server": "^10.45.2", + "add": "^2.0.6", + "bcryptjs": "^2.4.3", + "bits-ui": "^0.21.13", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "dayjs": "^1.11.9", + "dotenv": "^16.3.1", + "https-proxy-agent": "^7.0.5", + "ioredis": "^5.3.2", + "lucide-svelte": "^0.424.0", + "node-fetch": "^3.3.2", + "surrealdb.js": "^0.8.2", + "svelte-french-toast": "^1.1.0", + "svelte-headlessui": "^0.0.20", + "tailwind-merge": "^2.4.0", + "tailwind-variants": "^0.2.1", + "trpc-svelte-query-adapter": "^2.1.0", + "trpc-sveltekit": "^3.6.2", + "ulid": "^2.3.0", + "uuid": "^9.0.0", + "vite": "^5.3.5", + "zod": "^3.21.4" + }, + "devDependencies": { + "@iconify/json": "^2.2.91", + "@sveltejs/adapter-auto": "^2.1.0", + "@sveltejs/adapter-node": "^1.3.1", + "@sveltejs/kit": "^1.22.3", + "@tailwindcss/typography": "^0.5.13", + "@types/bcryptjs": "^2.4.2", + "@types/node": "^20.4.2", + "@types/uuid": "^9.0.2", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "svelte": "^4.0.5", + "svelte-check": "^3.4.6", + "tailwindcss": "^3.4.4", + "tslib": "^2.6.0", + "typescript": "^5.1.6", + "unplugin-auto-import": "^0.16.6", + "unplugin-icons": "^0.16.5", + "vitest": "^0.33.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100755 index 0000000..0c44873 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3543 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@melt-ui/svelte': + specifier: ^0.22.2 + version: 0.22.2(svelte@4.2.1) + '@sveltejs/svelte-virtual-list': + specifier: ^3.0.1 + version: 3.0.1 + '@tanstack/match-sorter-utils': + specifier: ^8.8.4 + version: 8.8.4 + '@tanstack/svelte-query': + specifier: ^4.35.3 + version: 4.35.3(svelte@4.2.1) + '@tanstack/svelte-table': + specifier: ^8.9.4 + version: 8.10.1(svelte@4.2.1) + '@trpc/client': + specifier: ^10.45.2 + version: 10.45.2(@trpc/server@10.45.2) + '@trpc/server': + specifier: ^10.45.2 + version: 10.45.2 + add: + specifier: ^2.0.6 + version: 2.0.6 + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + bits-ui: + specifier: ^0.21.13 + version: 0.21.13(svelte@4.2.1) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + date-fns-tz: + specifier: ^3.1.3 + version: 3.1.3(date-fns@3.6.0) + dayjs: + specifier: ^1.11.9 + version: 1.11.10 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + https-proxy-agent: + specifier: ^7.0.5 + version: 7.0.5 + ioredis: + specifier: ^5.3.2 + version: 5.3.2 + lucide-svelte: + specifier: ^0.424.0 + version: 0.424.0(svelte@4.2.1) + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + surrealdb.js: + specifier: ^0.8.2 + version: 0.8.4 + svelte-french-toast: + specifier: ^1.1.0 + version: 1.2.0(svelte@4.2.1) + svelte-headlessui: + specifier: ^0.0.20 + version: 0.0.20(svelte@4.2.1) + tailwind-merge: + specifier: ^2.4.0 + version: 2.4.0 + tailwind-variants: + specifier: ^0.2.1 + version: 0.2.1(tailwindcss@3.4.7) + trpc-svelte-query-adapter: + specifier: ^2.1.0 + version: 2.1.0 + trpc-sveltekit: + specifier: ^3.6.2 + version: 3.6.2(@sveltejs/adapter-node@1.3.1(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(ws@8.14.2) + ulid: + specifier: ^2.3.0 + version: 2.3.0 + uuid: + specifier: ^9.0.0 + version: 9.0.1 + vite: + specifier: ^5.3.5 + version: 5.3.5(@types/node@20.6.4) + zod: + specifier: ^3.21.4 + version: 3.22.2 + devDependencies: + '@iconify/json': + specifier: ^2.2.91 + version: 2.2.119 + '@sveltejs/adapter-auto': + specifier: ^2.1.0 + version: 2.1.0(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))) + '@sveltejs/adapter-node': + specifier: ^1.3.1 + version: 1.3.1(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))) + '@sveltejs/kit': + specifier: ^1.22.3 + version: 1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)) + '@tailwindcss/typography': + specifier: ^0.5.13 + version: 0.5.13(tailwindcss@3.4.7) + '@types/bcryptjs': + specifier: ^2.4.2 + version: 2.4.4 + '@types/node': + specifier: ^20.4.2 + version: 20.6.4 + '@types/uuid': + specifier: ^9.0.2 + version: 9.0.4 + autoprefixer: + specifier: ^10.4.19 + version: 10.4.20(postcss@8.4.40) + postcss: + specifier: ^8.4.38 + version: 8.4.40 + svelte: + specifier: ^4.0.5 + version: 4.2.1 + svelte-check: + specifier: ^3.4.6 + version: 3.5.2(postcss-load-config@4.0.1(postcss@8.4.40))(postcss@8.4.40)(svelte@4.2.1) + tailwindcss: + specifier: ^3.4.4 + version: 3.4.7 + tslib: + specifier: ^2.6.0 + version: 2.6.2 + typescript: + specifier: ^5.1.6 + version: 5.2.2 + unplugin-auto-import: + specifier: ^0.16.6 + version: 0.16.6(rollup@3.29.2) + unplugin-icons: + specifier: ^0.16.5 + version: 0.16.6 + vitest: + specifier: ^0.33.0 + version: 0.33.0 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.2.1': + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@0.1.1': + resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==} + + '@antfu/utils@0.7.6': + resolution: {integrity: sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.5.0': + resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} + + '@floating-ui/dom@1.5.3': + resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} + + '@floating-ui/utils@0.1.4': + resolution: {integrity: sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==} + + '@iconify/json@2.2.119': + resolution: {integrity: sha512-tYvxJpBds6UgXe6/iq2pW7hblsniDGWuKEwCr8PnSKLmZMilrtzHX7v+pg7FFmV2l2qA2Iw4toMRpe11tWBI4Q==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.1.10': + resolution: {integrity: sha512-0/+5hxjzCZ9RoYpqxnOzbnpQyMdZRuHcMxPJeuX+x/aZkAAD/N4TajDjAPT7LpX+M0bfLExj/p0bbDkUfp0lrg==} + + '@internationalized/date@3.5.5': + resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==} + + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.3': + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.1': + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.1.2': + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.19': + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + + '@melt-ui/svelte@0.22.2': + resolution: {integrity: sha512-mcXyoEW5/74y3QWnxHeSZgj+F/zG6mK4F//Wqig3KUGZ6uUEDwjkRCGDhY3EvL5n8MfY2P8uJWXiTZ16kLeh7g==} + peerDependencies: + svelte: '>=3 <5' + + '@melt-ui/svelte@0.76.2': + resolution: {integrity: sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==} + peerDependencies: + svelte: '>=3 <5' + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@polka/url@1.0.0-next.23': + resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + + '@rollup/plugin-commonjs@25.0.4': + resolution: {integrity: sha512-L92Vz9WUZXDnlQQl3EwbypJR4+DM2EbsO+/KOcEkP4Mc6Ct453EeDB2uH9lgRwj4w5yflgNpq9pHOiY8aoUXBQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.0.0': + resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@15.2.1': + resolution: {integrity: sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.0.4': + resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.20.0': + resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.20.0': + resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.20.0': + resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.20.0': + resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.20.0': + resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.20.0': + resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.20.0': + resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.20.0': + resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': + resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.20.0': + resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.20.0': + resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.20.0': + resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.20.0': + resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.20.0': + resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.20.0': + resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.20.0': + resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sveltejs/adapter-auto@2.1.0': + resolution: {integrity: sha512-o2pZCfATFtA/Gw/BB0Xm7k4EYaekXxaPGER3xGSY3FvzFJGTlJlZjBseaXwYSM94lZ0HniOjTokN3cWaLX6fow==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 + + '@sveltejs/adapter-node@1.3.1': + resolution: {integrity: sha512-A0VgRQDCDPzdLNoiAbcOxGw4zT1Mc+n1LwT1OmO350R7WxrEqdMUChPPOd1iMfIDWlP4ie6E2d/WQf5es2d4Zw==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 + + '@sveltejs/kit@1.25.0': + resolution: {integrity: sha512-+VqMWJJYtcLoF8hYkdqY2qs/MPaawrMwA/gNBJW2o2UrcuYdNiy0ZZnjQQuPD33df/VcAulnoeyzF5ZtaajFEw==} + engines: {node: ^16.14 || >=18} + hasBin: true + peerDependencies: + svelte: ^3.54.0 || ^4.0.0-next.0 + vite: ^4.0.0 + + '@sveltejs/svelte-virtual-list@3.0.1': + resolution: {integrity: sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ==} + + '@sveltejs/vite-plugin-svelte-inspector@1.0.4': + resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.2.0 + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + + '@sveltejs/vite-plugin-svelte@2.4.6': + resolution: {integrity: sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + + '@swc/helpers@0.5.12': + resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} + + '@tailwindcss/typography@0.5.13': + resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + '@tanstack/match-sorter-utils@8.8.4': + resolution: {integrity: sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==} + engines: {node: '>=12'} + + '@tanstack/query-core@4.35.3': + resolution: {integrity: sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==} + + '@tanstack/svelte-query@4.35.3': + resolution: {integrity: sha512-4gAWm6L+rofZOXZH//JKqKZoGOC1+/j0WgycpM9CwKKq/qIbXCoGW0r+0cO31s7O/F4v0qm+YZ3U7owOWfaiwQ==} + peerDependencies: + svelte: '>=3 <5' + + '@tanstack/svelte-table@8.10.1': + resolution: {integrity: sha512-JvCDqZ8iSMDX1qu2S76bE6QjyiOEr5VdlWiFTGXKIiD1WKEH1BOMddh+BPx6RUbUZddlhlZHlOfULA+FR48+hg==} + engines: {node: '>=12'} + peerDependencies: + svelte: ^4.0.0 || ^3.49.0 + + '@tanstack/table-core@8.10.1': + resolution: {integrity: sha512-dvO7wz+WjnT+7KI6ZZ+GAe9tljIFResDaV/TfOhfpeTB0ud9pILsavuM22HAXG2NsVaIG2Zax2OaVIsNt0z7Og==} + engines: {node: '>=12'} + + '@trpc/client@10.45.2': + resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==} + peerDependencies: + '@trpc/server': 10.45.2 + + '@trpc/server@10.45.2': + resolution: {integrity: sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==} + + '@types/bcryptjs@2.4.4': + resolution: {integrity: sha512-9wlJI7k5gRyJEC4yrV7DubzNQFTPiykYxUA6lBtsk5NlOfW9oWLJ1HdIA4YtE+6C3i3mTpDQQEosJ2rVZfBWnw==} + + '@types/chai-subset@1.3.3': + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + + '@types/chai@4.3.6': + resolution: {integrity: sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==} + + '@types/cookie@0.5.2': + resolution: {integrity: sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==} + + '@types/estree@1.0.2': + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/node@20.6.4': + resolution: {integrity: sha512-nU6d9MPY0NBUMiE/nXd2IIoC4OLvsLpwAjheoAeuzgvDZA1Cb10QYg+91AF6zQiKWRN5i1m07x6sMe0niBznoQ==} + + '@types/pug@2.0.6': + resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/uuid@9.0.4': + resolution: {integrity: sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==} + + '@vitest/expect@0.33.0': + resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==} + + '@vitest/runner@0.33.0': + resolution: {integrity: sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==} + + '@vitest/snapshot@0.33.0': + resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==} + + '@vitest/spy@0.33.0': + resolution: {integrity: sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==} + + '@vitest/utils@0.33.0': + resolution: {integrity: sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==} + + acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + + acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + + add@2.0.6: + resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + + binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + bits-ui@0.21.13: + resolution: {integrity: sha512-7nmOh6Ig7ND4DXZHv1FhNsY9yUGrad0+mf3tc4YN//3MgnJT1LnHtk4HZAKgmxCOe7txSX7/39LtYHbkrXokAQ==} + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.118 + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001647: + resolution: {integrity: sha512-n83xdNiyeNcHpzWY+1aFbqCK7LuLfBricc4+alSQL2Xb6OR3XpnQAmlDG+pQcdTfiHRuLcQ96VOfrPSGiNJYSg==} + + chai@4.3.8: + resolution: {integrity: sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==} + engines: {node: '>=4'} + + check-error@1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + + chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + date-fns-tz@3.1.3: + resolution: {integrity: sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==} + peerDependencies: + date-fns: ^3.0.0 + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + devalue@4.3.2: + resolution: {integrity: sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + + electron-to-chromium@1.5.4: + resolution: {integrity: sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==} + + es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esm-env@1.0.0: + resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + focus-trap@7.5.2: + resolution: {integrity: sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + get-func-name@2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-meta-resolve@3.0.0: + resolution: {integrity: sha512-4IwhLhNNA8yy445rPjD/lWh++7hMDOml2eHtd58eG7h+qK3EryMuuRbsHGPikCoAgIkkDnckKfWSk2iDla/ejg==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.3.2: + resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==} + engines: {node: '>=12.22.0'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + + lucide-svelte@0.424.0: + resolution: {integrity: sha512-mrEapPPlH5PWGMI0zs3dctR/6NylLx1VM9avRmH4OHN5aaldk4QYd/wTooROD4H6OKlC+7XOdgz0zX1tSxFlXw==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + + magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + + magic-string@0.30.3: + resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} + engines: {node: '>=12'} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mlly@1.4.2: + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + + nanoid@5.0.7: + resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} + engines: {node: ^18 || >=20} + hasBin: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.1: + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.0.1: + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.40: + resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + proxy-deep@3.1.1: + resolution: {integrity: sha512-kppbvLUNJ4IOMZds9/4gz/rtT5OFiesy3XosLsgMKlF3vb6GA5Y3ptyDlzKLcOcUBW+zaY+RiMINTsgE+O6e+Q==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + remove-accents@0.4.2: + resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + + rollup@3.29.2: + resolution: {integrity: sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.20.0: + resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + sander@0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + + scule@1.0.0: + resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} + + set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + + sorcery@0.11.0: + resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} + hasBin: true + + source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + std-env@3.4.3: + resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + + sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + surrealdb.js@0.8.4: + resolution: {integrity: sha512-ToCyBHxpVPGXth31ZktQvv+s7fvZG6+sR3mXHNAlhq0/43yYiYx3+3cYvCDGZQNBNUI42KENv8/aBQ5mGQZEEA==} + + svelte-check@3.5.2: + resolution: {integrity: sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==} + hasBin: true + peerDependencies: + svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 + + svelte-french-toast@1.2.0: + resolution: {integrity: sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==} + peerDependencies: + svelte: ^3.57.0 || ^4.0.0 + + svelte-headlessui@0.0.20: + resolution: {integrity: sha512-Jobm+E5PW9Ya0EG5ELl9/vU4u2zKh9edhQV4BpFVos53VBgiZbC8bhwr0fefFLLlZim8z/L7WVUl3RWz7DPYlg==} + + svelte-hmr@0.15.3: + resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: ^3.19.0 || ^4.0.0 + + svelte-preprocess@5.0.4: + resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} + engines: {node: '>= 14.10.0'} + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 + svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 + typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + + svelte-transition@0.0.10: + resolution: {integrity: sha512-BN8XDA7dKyuh+Lmdn3vxCzJd3M7L4BLdRziIAJew2AiBFMcrJJg8srEMYYoTCOLtYJ2Oqlv3+3/K5b6uHM8LSg==} + peerDependencies: + svelte: ^3.59.1 || ^4.0.0 + + svelte-writable-derived@3.1.0: + resolution: {integrity: sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==} + peerDependencies: + svelte: ^3.2.1 || ^4.0.0-next.1 + + svelte@3.59.2: + resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} + engines: {node: '>= 8'} + + svelte@4.2.1: + resolution: {integrity: sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==} + engines: {node: '>=16'} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwind-merge@2.4.0: + resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==} + + tailwind-variants@0.2.1: + resolution: {integrity: sha512-2xmhAf4UIc3PijOUcJPA1LP4AbxhpcHuHM2C26xM0k81r0maAO6uoUSHl3APmvHZcY5cZCY/bYuJdfFa4eGoaw==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwindcss: '*' + + tailwindcss@3.4.7: + resolution: {integrity: sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + + tinybench@2.5.1: + resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + + tinypool@0.6.0: + resolution: {integrity: sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==} + engines: {node: '>=14.0.0'} + + tinyspy@2.1.1: + resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + trpc-svelte-query-adapter@2.1.0: + resolution: {integrity: sha512-PQP/OurS8Vr6NtZyk0SOXry4yQaQpz9PBvnBHNHT7TMASyUavHqe/x95QuUlMsa6+KXN2o2bz2Qcz6Qt2JDBvQ==} + + trpc-sveltekit@3.6.2: + resolution: {integrity: sha512-wfcUGpasdSQjNaMnbaaHs4gnh5580oNoOYBsnWujE8+tXyyYAxYA2z6vrQmuG4/rOLdbiEns5ISfsejZz9l+2Q==} + peerDependencies: + '@sveltejs/adapter-node': '>=1.2' + '@trpc/client': ^10.0.0 + '@trpc/server': ^10.0.0 + ws: '>=8' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.3.0: + resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==} + + ulid@2.3.0: + resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + hasBin: true + + undici@5.23.0: + resolution: {integrity: sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==} + engines: {node: '>=14.0'} + + unimport@3.3.0: + resolution: {integrity: sha512-3jhq3ZG5hFZzrWGDCpx83kjPzefP/EeuKkIO1T0MA4Zwj+dO/Og1mFvZ4aZ5WSDm0FVbbdVIRH1zKBG7c4wOpg==} + + unplugin-auto-import@0.16.6: + resolution: {integrity: sha512-M+YIITkx3C/Hg38hp8HmswP5mShUUyJOzpifv7RTlAbeFlO2Tyw0pwrogSSxnipHDPTtI8VHFBpkYkNKzYSuyA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-icons@0.16.6: + resolution: {integrity: sha512-jL70sAC7twp4hI/MTfm+vyvTRtHqiEIzf3XOjJz7yzhMEEQnk5Ey5YIXRAU03Mc4BF99ITvvnBzfyRZee86OeA==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + + unplugin@1.5.0: + resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==} + + unws@0.2.4: + resolution: {integrity: sha512-/N1ajiqrSp0A/26/LBg7r10fOcPtGXCqJRJ61sijUFoGZMr6ESWGYn7i0cwr7fR7eEECY5HsitqtjGHDZLAu2w==} + engines: {node: '>=16.14.0'} + peerDependencies: + ws: '*' + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vite-node@0.33.0: + resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} + engines: {node: '>=v14.18.0'} + hasBin: true + + vite@4.4.9: + resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@5.3.5: + resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@0.2.4: + resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@0.33.0: + resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.3.2: + resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==} + engines: {node: '>= 14'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + + zod@3.22.2: + resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.2.1': + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + + '@antfu/install-pkg@0.1.1': + dependencies: + execa: 5.1.1 + find-up: 5.0.0 + + '@antfu/utils@0.7.6': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@floating-ui/core@1.5.0': + dependencies: + '@floating-ui/utils': 0.1.4 + + '@floating-ui/dom@1.5.3': + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/utils': 0.1.4 + + '@floating-ui/utils@0.1.4': {} + + '@iconify/json@2.2.119': + dependencies: + '@iconify/types': 2.0.0 + pathe: 1.1.1 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.1.10': + dependencies: + '@antfu/install-pkg': 0.1.1 + '@antfu/utils': 0.7.6 + '@iconify/types': 2.0.0 + debug: 4.3.4 + kolorist: 1.8.0 + local-pkg: 0.4.3 + transitivePeerDependencies: + - supports-color + + '@internationalized/date@3.5.5': + dependencies: + '@swc/helpers': 0.5.12 + + '@ioredis/commands@1.2.0': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jridgewell/gen-mapping@0.3.3': + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + + '@jridgewell/resolve-uri@3.1.1': {} + + '@jridgewell/set-array@1.1.2': {} + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@jridgewell/trace-mapping@0.3.19': + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@melt-ui/svelte@0.22.2(svelte@4.2.1)': + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/dom': 1.5.3 + focus-trap: 7.5.2 + nanoid: 4.0.2 + svelte: 4.2.1 + + '@melt-ui/svelte@0.76.2(svelte@4.2.1)': + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/dom': 1.5.3 + '@internationalized/date': 3.5.5 + dequal: 2.0.3 + focus-trap: 7.5.2 + nanoid: 5.0.7 + svelte: 4.2.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + + '@polka/url@1.0.0-next.23': {} + + '@rollup/plugin-commonjs@25.0.4(rollup@3.29.2)': + dependencies: + '@rollup/pluginutils': 5.0.4(rollup@3.29.2) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.27.0 + optionalDependencies: + rollup: 3.29.2 + + '@rollup/plugin-json@6.0.0(rollup@3.29.2)': + dependencies: + '@rollup/pluginutils': 5.0.4(rollup@3.29.2) + optionalDependencies: + rollup: 3.29.2 + + '@rollup/plugin-node-resolve@15.2.1(rollup@3.29.2)': + dependencies: + '@rollup/pluginutils': 5.0.4(rollup@3.29.2) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.6 + optionalDependencies: + rollup: 3.29.2 + + '@rollup/pluginutils@5.0.4(rollup@3.29.2)': + dependencies: + '@types/estree': 1.0.2 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 3.29.2 + + '@rollup/rollup-android-arm-eabi@4.20.0': + optional: true + + '@rollup/rollup-android-arm64@4.20.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.20.0': + optional: true + + '@rollup/rollup-darwin-x64@4.20.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.20.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.20.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.20.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.20.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.20.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.20.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.20.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.20.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.20.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.20.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.20.0': + optional: true + + '@sinclair/typebox@0.27.8': {} + + '@sveltejs/adapter-auto@2.1.0(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)))': + dependencies: + '@sveltejs/kit': 1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)) + import-meta-resolve: 3.0.0 + + '@sveltejs/adapter-node@1.3.1(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)))': + dependencies: + '@rollup/plugin-commonjs': 25.0.4(rollup@3.29.2) + '@rollup/plugin-json': 6.0.0(rollup@3.29.2) + '@rollup/plugin-node-resolve': 15.2.1(rollup@3.29.2) + '@sveltejs/kit': 1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)) + rollup: 3.29.2 + + '@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))': + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)) + '@types/cookie': 0.5.2 + cookie: 0.5.0 + devalue: 4.3.2 + esm-env: 1.0.0 + kleur: 4.1.5 + magic-string: 0.30.3 + mime: 3.0.0 + sade: 1.8.1 + set-cookie-parser: 2.6.0 + sirv: 2.0.3 + svelte: 4.2.1 + tiny-glob: 0.2.9 + undici: 5.23.0 + vite: 5.3.5(@types/node@20.6.4) + transitivePeerDependencies: + - supports-color + + '@sveltejs/svelte-virtual-list@3.0.1': {} + + '@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)))(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))': + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)) + debug: 4.3.4 + svelte: 4.2.1 + vite: 5.3.5(@types/node@20.6.4) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)))(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4)) + debug: 4.3.4 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.3 + svelte: 4.2.1 + svelte-hmr: 0.15.3(svelte@4.2.1) + vite: 5.3.5(@types/node@20.6.4) + vitefu: 0.2.4(vite@5.3.5(@types/node@20.6.4)) + transitivePeerDependencies: + - supports-color + + '@swc/helpers@0.5.12': + dependencies: + tslib: 2.6.2 + + '@tailwindcss/typography@0.5.13(tailwindcss@3.4.7)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.7 + + '@tanstack/match-sorter-utils@8.8.4': + dependencies: + remove-accents: 0.4.2 + + '@tanstack/query-core@4.35.3': {} + + '@tanstack/svelte-query@4.35.3(svelte@3.59.2)': + dependencies: + '@tanstack/query-core': 4.35.3 + svelte: 3.59.2 + + '@tanstack/svelte-query@4.35.3(svelte@4.2.1)': + dependencies: + '@tanstack/query-core': 4.35.3 + svelte: 4.2.1 + + '@tanstack/svelte-table@8.10.1(svelte@4.2.1)': + dependencies: + '@tanstack/table-core': 8.10.1 + svelte: 4.2.1 + + '@tanstack/table-core@8.10.1': {} + + '@trpc/client@10.45.2(@trpc/server@10.45.2)': + dependencies: + '@trpc/server': 10.45.2 + + '@trpc/server@10.45.2': {} + + '@types/bcryptjs@2.4.4': {} + + '@types/chai-subset@1.3.3': + dependencies: + '@types/chai': 4.3.6 + + '@types/chai@4.3.6': {} + + '@types/cookie@0.5.2': {} + + '@types/estree@1.0.2': {} + + '@types/estree@1.0.5': {} + + '@types/node@20.6.4': {} + + '@types/pug@2.0.6': {} + + '@types/resolve@1.20.2': {} + + '@types/uuid@9.0.4': {} + + '@vitest/expect@0.33.0': + dependencies: + '@vitest/spy': 0.33.0 + '@vitest/utils': 0.33.0 + chai: 4.3.8 + + '@vitest/runner@0.33.0': + dependencies: + '@vitest/utils': 0.33.0 + p-limit: 4.0.0 + pathe: 1.1.1 + + '@vitest/snapshot@0.33.0': + dependencies: + magic-string: 0.30.3 + pathe: 1.1.1 + pretty-format: 29.7.0 + + '@vitest/spy@0.33.0': + dependencies: + tinyspy: 2.1.1 + + '@vitest/utils@0.33.0': + dependencies: + diff-sequences: 29.6.3 + loupe: 2.3.6 + pretty-format: 29.7.0 + + acorn-walk@8.2.0: {} + + acorn@8.10.0: {} + + add@2.0.6: {} + + agent-base@7.1.1: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + ansi-styles@5.2.0: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@1.1.0: {} + + autoprefixer@10.4.20(postcss@8.4.40): + dependencies: + browserslist: 4.23.3 + caniuse-lite: 1.0.30001647 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.40 + postcss-value-parser: 4.2.0 + + axobject-query@3.2.1: + dependencies: + dequal: 2.0.3 + + balanced-match@1.0.2: {} + + bcryptjs@2.4.3: {} + + binary-extensions@2.2.0: {} + + bits-ui@0.21.13(svelte@4.2.1): + dependencies: + '@internationalized/date': 3.5.5 + '@melt-ui/svelte': 0.76.2(svelte@4.2.1) + nanoid: 5.0.7 + svelte: 4.2.1 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.2: + dependencies: + fill-range: 7.0.1 + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001647 + electron-to-chromium: 1.5.4 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + buffer-crc32@0.2.13: {} + + builtin-modules@3.3.0: {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001647: {} + + chai@4.3.8: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + + check-error@1.0.2: {} + + chokidar@3.5.3: + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + code-red@1.0.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + '@types/estree': 1.0.2 + acorn: 8.10.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + + commander@4.1.1: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + cookie@0.5.0: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + + cssesc@3.0.0: {} + + data-uri-to-buffer@4.0.1: {} + + date-fns-tz@3.1.3(date-fns@3.6.0): + dependencies: + date-fns: 3.6.0 + + date-fns@3.6.0: {} + + dayjs@1.11.10: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + deep-eql@4.1.3: + dependencies: + type-detect: 4.0.8 + + deepmerge@4.3.1: {} + + denque@2.1.0: {} + + dequal@2.0.3: {} + + detect-indent@6.1.0: {} + + devalue@4.3.2: {} + + didyoumean@1.2.2: {} + + diff-sequences@29.6.3: {} + + dlv@1.1.3: {} + + dotenv@16.3.1: {} + + electron-to-chromium@1.5.4: {} + + es6-promise@3.3.1: {} + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.1.2: {} + + escape-string-regexp@5.0.0: {} + + esm-env@1.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.2 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + + fastq@1.15.0: + dependencies: + reusify: 1.0.4 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + fill-range@7.0.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + focus-trap@7.5.2: + dependencies: + tabbable: 6.2.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fraction.js@4.3.7: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.1: {} + + get-func-name@2.0.0: {} + + get-stream@6.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + + graceful-fs@4.2.11: {} + + has@1.0.3: + dependencies: + function-bind: 1.1.1 + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@3.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ioredis@5.3.2: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.2.0 + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-core-module@2.13.0: + dependencies: + has: 1.0.3 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-module@1.0.0: {} + + is-number@7.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.2 + + is-reference@3.0.2: + dependencies: + '@types/estree': 1.0.2 + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + jiti@1.21.6: {} + + jsonc-parser@3.2.0: {} + + kleur@4.1.5: {} + + kolorist@1.8.0: {} + + lilconfig@2.1.0: {} + + lines-and-columns@1.2.4: {} + + local-pkg@0.4.3: {} + + locate-character@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.castarray@4.4.0: {} + + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + + loupe@2.3.6: + dependencies: + get-func-name: 2.0.0 + + lucide-svelte@0.424.0(svelte@4.2.1): + dependencies: + svelte: 4.2.1 + + magic-string@0.27.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + + magic-string@0.30.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + + mdn-data@2.0.30: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.5: + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + mime@3.0.0: {} + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mlly@1.4.2: + dependencies: + acorn: 8.10.0 + pathe: 1.1.1 + pkg-types: 1.0.3 + ufo: 1.3.0 + + mri@1.2.0: {} + + mrmime@1.0.1: {} + + ms@2.1.2: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.7: {} + + nanoid@4.0.2: {} + + nanoid@5.0.7: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.18: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.0.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@1.1.1: {} + + pathval@1.1.1: {} + + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.2 + estree-walker: 3.0.3 + is-reference: 3.0.2 + + picocolors@1.0.0: {} + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + pirates@4.0.6: {} + + pkg-types@1.0.3: + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.4.2 + pathe: 1.1.1 + + postcss-import@15.1.0(postcss@8.4.40): + dependencies: + postcss: 8.4.40 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.6 + + postcss-js@4.0.1(postcss@8.4.40): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.40 + + postcss-load-config@4.0.1(postcss@8.4.40): + dependencies: + lilconfig: 2.1.0 + yaml: 2.3.2 + optionalDependencies: + postcss: 8.4.40 + + postcss-nested@6.0.1(postcss@8.4.40): + dependencies: + postcss: 8.4.40 + postcss-selector-parser: 6.0.13 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.0.13: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.40: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + proxy-deep@3.1.1: {} + + queue-microtask@1.2.3: {} + + react-is@18.2.0: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + remove-accents@0.4.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.6: + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rollup@3.29.2: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.20.0: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.20.0 + '@rollup/rollup-android-arm64': 4.20.0 + '@rollup/rollup-darwin-arm64': 4.20.0 + '@rollup/rollup-darwin-x64': 4.20.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.20.0 + '@rollup/rollup-linux-arm-musleabihf': 4.20.0 + '@rollup/rollup-linux-arm64-gnu': 4.20.0 + '@rollup/rollup-linux-arm64-musl': 4.20.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.20.0 + '@rollup/rollup-linux-riscv64-gnu': 4.20.0 + '@rollup/rollup-linux-s390x-gnu': 4.20.0 + '@rollup/rollup-linux-x64-gnu': 4.20.0 + '@rollup/rollup-linux-x64-musl': 4.20.0 + '@rollup/rollup-win32-arm64-msvc': 4.20.0 + '@rollup/rollup-win32-ia32-msvc': 4.20.0 + '@rollup/rollup-win32-x64-msvc': 4.20.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + sander@0.5.1: + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.11 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + scule@1.0.0: {} + + set-cookie-parser@2.6.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + sirv@2.0.3: + dependencies: + '@polka/url': 1.0.0-next.23 + mrmime: 1.0.1 + totalist: 3.0.1 + + sorcery@0.11.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + buffer-crc32: 0.2.13 + minimist: 1.2.8 + sander: 0.5.1 + + source-map-js@1.0.2: {} + + source-map-js@1.2.0: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + std-env@3.4.3: {} + + streamsearch@1.1.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-literal@1.3.0: + dependencies: + acorn: 8.10.0 + + sucrase@3.34.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + surrealdb.js@0.8.4: + dependencies: + unws: 0.2.4(ws@8.14.2) + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + svelte-check@3.5.2(postcss-load-config@4.0.1(postcss@8.4.40))(postcss@8.4.40)(svelte@4.2.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + chokidar: 3.5.3 + fast-glob: 3.3.1 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 4.2.1 + svelte-preprocess: 5.0.4(postcss-load-config@4.0.1(postcss@8.4.40))(postcss@8.4.40)(svelte@4.2.1)(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + + svelte-french-toast@1.2.0(svelte@4.2.1): + dependencies: + svelte: 4.2.1 + svelte-writable-derived: 3.1.0(svelte@4.2.1) + + svelte-headlessui@0.0.20(svelte@4.2.1): + dependencies: + svelte-transition: 0.0.10(svelte@4.2.1) + transitivePeerDependencies: + - svelte + + svelte-hmr@0.15.3(svelte@4.2.1): + dependencies: + svelte: 4.2.1 + + svelte-preprocess@5.0.4(postcss-load-config@4.0.1(postcss@8.4.40))(postcss@8.4.40)(svelte@4.2.1)(typescript@5.2.2): + dependencies: + '@types/pug': 2.0.6 + detect-indent: 6.1.0 + magic-string: 0.27.0 + sorcery: 0.11.0 + strip-indent: 3.0.0 + svelte: 4.2.1 + optionalDependencies: + postcss: 8.4.40 + postcss-load-config: 4.0.1(postcss@8.4.40) + typescript: 5.2.2 + + svelte-transition@0.0.10(svelte@4.2.1): + dependencies: + svelte: 4.2.1 + + svelte-writable-derived@3.1.0(svelte@4.2.1): + dependencies: + svelte: 4.2.1 + + svelte@3.59.2: {} + + svelte@4.2.1: + dependencies: + '@ampproject/remapping': 2.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + acorn: 8.10.0 + aria-query: 5.3.0 + axobject-query: 3.2.1 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.2 + locate-character: 3.0.0 + magic-string: 0.30.3 + periscopic: 3.1.0 + + tabbable@6.2.0: {} + + tailwind-merge@2.4.0: {} + + tailwind-variants@0.2.1(tailwindcss@3.4.7): + dependencies: + tailwind-merge: 2.4.0 + tailwindcss: 3.4.7 + + tailwindcss@3.4.7: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.1 + postcss: 8.4.40 + postcss-import: 15.1.0(postcss@8.4.40) + postcss-js: 4.0.1(postcss@8.4.40) + postcss-load-config: 4.0.1(postcss@8.4.40) + postcss-nested: 6.0.1(postcss@8.4.40) + postcss-selector-parser: 6.0.13 + resolve: 1.22.6 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + + tinybench@2.5.1: {} + + tinypool@0.6.0: {} + + tinyspy@2.1.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + trpc-svelte-query-adapter@2.1.0: + dependencies: + '@tanstack/svelte-query': 4.35.3(svelte@3.59.2) + '@trpc/client': 10.45.2(@trpc/server@10.45.2) + '@trpc/server': 10.45.2 + proxy-deep: 3.1.1 + svelte: 3.59.2 + + trpc-sveltekit@3.6.2(@sveltejs/adapter-node@1.3.1(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(ws@8.14.2): + dependencies: + '@sveltejs/adapter-node': 1.3.1(@sveltejs/kit@1.25.0(svelte@4.2.1)(vite@5.3.5(@types/node@20.6.4))) + '@trpc/client': 10.45.2(@trpc/server@10.45.2) + '@trpc/server': 10.45.2 + ws: 8.14.2 + + ts-interface-checker@0.1.13: {} + + tslib@2.6.2: {} + + type-detect@4.0.8: {} + + typescript@5.2.2: {} + + ufo@1.3.0: {} + + ulid@2.3.0: {} + + undici@5.23.0: + dependencies: + busboy: 1.6.0 + + unimport@3.3.0(rollup@3.29.2): + dependencies: + '@rollup/pluginutils': 5.0.4(rollup@3.29.2) + escape-string-regexp: 5.0.0 + fast-glob: 3.3.1 + local-pkg: 0.4.3 + magic-string: 0.30.3 + mlly: 1.4.2 + pathe: 1.1.1 + pkg-types: 1.0.3 + scule: 1.0.0 + strip-literal: 1.3.0 + unplugin: 1.5.0 + transitivePeerDependencies: + - rollup + + unplugin-auto-import@0.16.6(rollup@3.29.2): + dependencies: + '@antfu/utils': 0.7.6 + '@rollup/pluginutils': 5.0.4(rollup@3.29.2) + fast-glob: 3.3.1 + local-pkg: 0.4.3 + magic-string: 0.30.3 + minimatch: 9.0.3 + unimport: 3.3.0(rollup@3.29.2) + unplugin: 1.5.0 + transitivePeerDependencies: + - rollup + + unplugin-icons@0.16.6: + dependencies: + '@antfu/install-pkg': 0.1.1 + '@antfu/utils': 0.7.6 + '@iconify/utils': 2.1.10 + debug: 4.3.4 + kolorist: 1.8.0 + local-pkg: 0.4.3 + unplugin: 1.5.0 + transitivePeerDependencies: + - supports-color + + unplugin@1.5.0: + dependencies: + acorn: 8.10.0 + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + + unws@0.2.4(ws@8.14.2): + dependencies: + ws: 8.14.2 + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.1.2 + picocolors: 1.0.1 + + util-deprecate@1.0.2: {} + + uuid@9.0.1: {} + + vite-node@0.33.0(@types/node@20.6.4): + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.4.2 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 4.4.9(@types/node@20.6.4) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vite@4.4.9(@types/node@20.6.4): + dependencies: + esbuild: 0.18.20 + postcss: 8.4.40 + rollup: 3.29.2 + optionalDependencies: + '@types/node': 20.6.4 + fsevents: 2.3.3 + + vite@5.3.5(@types/node@20.6.4): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.40 + rollup: 4.20.0 + optionalDependencies: + '@types/node': 20.6.4 + fsevents: 2.3.3 + + vitefu@0.2.4(vite@5.3.5(@types/node@20.6.4)): + optionalDependencies: + vite: 5.3.5(@types/node@20.6.4) + + vitest@0.33.0: + dependencies: + '@types/chai': 4.3.6 + '@types/chai-subset': 1.3.3 + '@types/node': 20.6.4 + '@vitest/expect': 0.33.0 + '@vitest/runner': 0.33.0 + '@vitest/snapshot': 0.33.0 + '@vitest/spy': 0.33.0 + '@vitest/utils': 0.33.0 + acorn: 8.10.0 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.8 + debug: 4.3.4 + local-pkg: 0.4.3 + magic-string: 0.30.3 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.4.3 + strip-literal: 1.3.0 + tinybench: 2.5.1 + tinypool: 0.6.0 + vite: 4.4.9(@types/node@20.6.4) + vite-node: 0.33.0(@types/node@20.6.4) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + web-streams-polyfill@3.3.3: {} + + webpack-sources@3.2.3: {} + + webpack-virtual-modules@0.5.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.2.2: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrappy@1.0.2: {} + + ws@8.14.2: {} + + yaml@2.3.2: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.0.0: {} + + zod@3.22.2: {} diff --git a/postcss.config.js b/postcss.config.js new file mode 100755 index 0000000..ba80730 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/src/__tests__/postdata.test.ts b/src/__tests__/postdata.test.ts new file mode 100644 index 0000000..4865a1a --- /dev/null +++ b/src/__tests__/postdata.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "vitest"; +import { + generatePostDataArrayFromBaseInfo, + rebalancePostDataListByBalanceOfUsers, +} from "$lib/server/postdata/postdata.gen.controller"; + +import type { ApiPostUser, PostDataEntry } from "$lib/utils/data.types"; +import { buildMessageString } from "$lib/server/postdata/post.handler"; + +test("test rebalancePostDataListByBalanceOfUsers", () => { + // empty case + + const testA = {} as Record; + const testB = [] as any[]; + const testC = [] as any[]; + + rebalancePostDataListByBalanceOfUsers(testA, testB, testC); + + expect(testA).toEqual({}); + expect(testB).toEqual([]); + expect(testC).toEqual([]); + + const balanceCounts: Record = {}; + const users: ApiPostUser[] = []; + const result: PostDataEntry[] = []; +}); + +test("post body message string generation function", () => { + let i = 0, + data = [] as PostDataEntry[], + distId = 1, + dealerId = 2, + drawId = 69, + date = new Date().toISOString().split("T")[0]; + let out = buildMessageString(i, data, distId, dealerId, drawId, date); + expect(out.message).toEqual(""); + + // data.push({ + // id: "123123", + // first: 10, + // second: 20, + // number: "1234", + // }); + // out = buildMessageString(i, data, distId, dealerId, drawId, date); + // expect(out.message).toEqual(`1,1,2,${drawId},${date},1234,10,20,30`); + + // data.push({ + // id: "123124", + // first: 0, + // second: 5, + // number: "0987", + // }); + // out = buildMessageString(i, data, distId, dealerId, drawId, date); + // expect(out.message).toEqual( + // `1,1,2,${drawId},${date},1234,10,20,30;2,1,2,${drawId},${date},0987,0,5,5`, + // ); +}); diff --git a/src/app.css b/src/app.css new file mode 100755 index 0000000..a8b6561 --- /dev/null +++ b/src/app.css @@ -0,0 +1,90 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: "Inter"; + src: url("/fonts/InterV.ttf"); +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + + --border: 220 13% 91%; + --input: 220 13% 91%; + + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 210 20% 98%; + + --ring: 224 71.4% 4.1%; + + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + font-family: + "Inter", + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Oxygen, + Ubuntu, + Cantarell, + "Open Sans", + "Helvetica Neue", + sans-serif; + } +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; + /* <-- Apparently some margin are still there even though it's hidden */ +} + +input[type="number"] { + -moz-appearance: textfield; + /* Firefox */ +} + +select { + /* for Firefox */ + -moz-appearance: none; + /* for Safari, Chrome, Opera */ + -webkit-appearance: none; +} + +select::-ms-expand { + display: none; +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100755 index 0000000..b4cf122 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,18 @@ +/// +/// +import type { SessionData } from "$lib/server/cookie.functions"; + +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + user?: SessionData; + } + // interface PageData {} + // interface Platform {} + } +} + +export { }; diff --git a/src/app.html b/src/app.html new file mode 100755 index 0000000..6e8ce1f --- /dev/null +++ b/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + + diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts new file mode 100755 index 0000000..e6fe5cf --- /dev/null +++ b/src/auto-imports.d.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const afterUpdate: typeof import('svelte')['afterUpdate'] + const beforeUpdate: typeof import('svelte')['beforeUpdate'] + const createEventDispatcher: typeof import('svelte')['createEventDispatcher'] + const first: typeof import('./hooks.server')['first'] + const getAllContexts: typeof import('svelte')['getAllContexts'] + const getContext: typeof import('svelte')['getContext'] + const handle: typeof import('./hooks.server')['handle'] + const handleError: typeof import('./hooks.server')['handleError'] + const hasContext: typeof import('svelte')['hasContext'] + const onDestroy: typeof import('svelte')['onDestroy'] + const onMount: typeof import('svelte')['onMount'] + const second: typeof import('./hooks.server')['second'] + const setContext: typeof import('svelte')['setContext'] + const tick: typeof import('svelte')['tick'] +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100755 index 0000000..14e2cb0 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,66 @@ +import type { Handle, HandleServerError } from "@sveltejs/kit"; +import { + getParsedSession, + parseCookieString, + type SessionData, +} from "$lib/server/cookie.functions"; +import { createContext } from "$lib/trpc/context"; +import { router } from "$lib/trpc/router"; +import { createTRPCHandle } from "trpc-sveltekit"; +import { sequence } from "@sveltejs/kit/hooks"; + +export const handleError: HandleServerError = async ({ error, event }) => { + console.log("[-] Running error middleware for : ", event.url.pathname); + console.log(error); + return { + message: (error as Error).message, + }; +}; + +export const first: Handle = createTRPCHandle({ router, createContext }); + +export const second: Handle = async ({ resolve, event }) => { + if ( + event.url.pathname.includes("/api/auth") || + event.url.pathname.includes("/api/debug") || + event.url.pathname.includes("/trpc") || + event.url.pathname.includes("/auth") + ) { + return await resolve(event); + } + console.log("[+] Running hook middleware for : ", event.url.pathname); + const sessionId = parseCookieString( + event.request.headers.get("cookie") || "", + ).SID; + const baseUrl = event.url.origin; + const signInUrl = baseUrl + "/auth/signin"; + const isSignInPage = event.url.pathname === "/auth/signin"; + const redirectResponse = new Response(null, { + status: 302, + headers: { + Location: signInUrl, + "Set-Cookie": sessionId + ? "SID=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT" + : "", + }, + }); + if (!sessionId && !isSignInPage) { + return redirectResponse; + } + const user = await getParsedSession(sessionId); + if (!user && !isSignInPage) { + return redirectResponse; + } else if (user && isSignInPage) { + return new Response(null, { + status: 302, + headers: { + Location: baseUrl, + }, + }); + } + event.locals.user = (user ?? undefined) as SessionData | undefined; + // optional: make this a variable to run code after the request is prepared + return await resolve(event); +}; + +export const handle = sequence(first, second); diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100755 index 0000000..e07cbbd --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/src/lib/components/atoms/button.styles.ts b/src/lib/components/atoms/button.styles.ts new file mode 100644 index 0000000..f6466b2 --- /dev/null +++ b/src/lib/components/atoms/button.styles.ts @@ -0,0 +1,35 @@ +import clsx from "clsx"; +import { tv } from "tailwind-variants"; + +export const buttonStyles = tv( + { + base: clsx( + "p-2 px-4 flex gap-1.5 items-center justify-center rounded-md transition-all duration-150 shadow-sm font-medium tracking-wide", + ), + variants: { + intent: { + primary: clsx("bg-sky-500 text-white hover:bg-sky-600"), + secondary: "bg-sky-800 text-white", + success: "bg-emerald-500 text-white hover:bg-emerald-600", + danger: "bg-rose-500 text-white hover:bg-rose-600", + warning: "bg-amber-500 text-white hover:bg-amber-600", + ghost: clsx( + "bg-slate-100 text-slate-500 hover:text-slate-600 hover:bg-slate-200", + ), + primaryInverted: clsx("bg-sky-50 text-sky-600 hover:bg-sky-100"), + successInverted: clsx( + "bg-emerald-50 hover:bg-emerald-100 text-emerald-600", + ), + dangerInverted: clsx("bg-slate-100 hover:bg-slate-200 text-rose-600"), + warningInverted: clsx("bg-amber-50 hover:bg-amber-100 text-amber-600"), + }, + size: { sm: "text-sm", md: "text-md", lg: "text-lg" }, + fullwidth: { yes: "w-full", max: "w-max", no: "" }, + }, + defaultVariants: { + intent: "primary", + size: "sm", + }, + }, + {}, +); diff --git a/src/lib/components/atoms/button.svelte b/src/lib/components/atoms/button.svelte new file mode 100755 index 0000000..ea4ae32 --- /dev/null +++ b/src/lib/components/atoms/button.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/components/atoms/checkbox.svelte b/src/lib/components/atoms/checkbox.svelte new file mode 100755 index 0000000..57dd4be --- /dev/null +++ b/src/lib/components/atoms/checkbox.svelte @@ -0,0 +1,43 @@ + + +
+ +
diff --git a/src/lib/components/atoms/fab.svelte b/src/lib/components/atoms/fab.svelte new file mode 100755 index 0000000..fb75a6d --- /dev/null +++ b/src/lib/components/atoms/fab.svelte @@ -0,0 +1,48 @@ + + + diff --git a/src/lib/components/atoms/headless.select.svelte b/src/lib/components/atoms/headless.select.svelte new file mode 100755 index 0000000..47fb469 --- /dev/null +++ b/src/lib/components/atoms/headless.select.svelte @@ -0,0 +1,107 @@ + + +
+ + + +
+
    + {#each options as value} + {@const chosen = selected.find((item) => item.value === value.value)} + {@const active = $listbox.active === value} +
  • + {value.label} + {#if chosen} + + + + {/if} +
  • + {/each} +
+
+
diff --git a/src/lib/components/atoms/icon-button.svelte b/src/lib/components/atoms/icon-button.svelte new file mode 100755 index 0000000..bd1bd72 --- /dev/null +++ b/src/lib/components/atoms/icon-button.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/atoms/input.svelte b/src/lib/components/atoms/input.svelte new file mode 100755 index 0000000..3283692 --- /dev/null +++ b/src/lib/components/atoms/input.svelte @@ -0,0 +1,143 @@ + + + diff --git a/src/lib/components/atoms/line.svelte b/src/lib/components/atoms/line.svelte new file mode 100755 index 0000000..48f0c0a --- /dev/null +++ b/src/lib/components/atoms/line.svelte @@ -0,0 +1,7 @@ + + +
diff --git a/src/lib/components/atoms/link-button.svelte b/src/lib/components/atoms/link-button.svelte new file mode 100755 index 0000000..8c021a3 --- /dev/null +++ b/src/lib/components/atoms/link-button.svelte @@ -0,0 +1,56 @@ + + +{#if disabled} +
+ {#if iconleft} + + {/if} + + {text} + + {#if iconright} + + {/if} +
+ ); +{:else} + + {#if iconleft} + + {/if} + + {text} + + {#if iconright} + + {/if} + +{/if} diff --git a/src/lib/components/atoms/modal.svelte b/src/lib/components/atoms/modal.svelte new file mode 100755 index 0000000..da2dab0 --- /dev/null +++ b/src/lib/components/atoms/modal.svelte @@ -0,0 +1,89 @@ + + +
+ +
+ {#if $open} +
+
+

+ Edit profile +

+

+ Make changes to your profile here. Click save when you're done. +

+ +
+ + +
+
+ + +
+
+ + +
+ + +
+ {/if} +
+
diff --git a/src/lib/components/atoms/navigation-links.svelte b/src/lib/components/atoms/navigation-links.svelte new file mode 100755 index 0000000..ed2d306 --- /dev/null +++ b/src/lib/components/atoms/navigation-links.svelte @@ -0,0 +1,30 @@ + + +{#each links as link} + +
+ +

{link.label}

+
+
+{/each} +{#each functionalLinks as link} + +{/each} diff --git a/src/lib/components/atoms/pagination.svelte b/src/lib/components/atoms/pagination.svelte new file mode 100755 index 0000000..cb277b9 --- /dev/null +++ b/src/lib/components/atoms/pagination.svelte @@ -0,0 +1,82 @@ + + + diff --git a/src/lib/components/atoms/pill.svelte b/src/lib/components/atoms/pill.svelte new file mode 100755 index 0000000..6316a81 --- /dev/null +++ b/src/lib/components/atoms/pill.svelte @@ -0,0 +1,25 @@ + + +
+ {text} +
diff --git a/src/lib/components/atoms/select.svelte b/src/lib/components/atoms/select.svelte new file mode 100755 index 0000000..fc9863a --- /dev/null +++ b/src/lib/components/atoms/select.svelte @@ -0,0 +1,39 @@ + + +
+ + +
diff --git a/src/lib/components/atoms/skeleton-loader.svelte b/src/lib/components/atoms/skeleton-loader.svelte new file mode 100755 index 0000000..eddc78d --- /dev/null +++ b/src/lib/components/atoms/skeleton-loader.svelte @@ -0,0 +1,25 @@ + + +
+ loader +
diff --git a/src/lib/components/atoms/switch.svelte b/src/lib/components/atoms/switch.svelte new file mode 100755 index 0000000..4623c65 --- /dev/null +++ b/src/lib/components/atoms/switch.svelte @@ -0,0 +1,65 @@ + + +
+
+ + + {label} + +
+
diff --git a/src/lib/components/atoms/title.svelte b/src/lib/components/atoms/title.svelte new file mode 100755 index 0000000..58539f8 --- /dev/null +++ b/src/lib/components/atoms/title.svelte @@ -0,0 +1,59 @@ + + +{#if size == "h1"} +

+ {text} +

+{:else if size == "h2"} +

+ {text} +

+{:else if size == "h3"} +

+ {text} +

+{:else if size == "h4"} +

+ {text} +

+{/if} diff --git a/src/lib/components/molecules/centered-spinner.svelte b/src/lib/components/molecules/centered-spinner.svelte new file mode 100755 index 0000000..ffa4477 --- /dev/null +++ b/src/lib/components/molecules/centered-spinner.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/lib/components/molecules/loader.svelte b/src/lib/components/molecules/loader.svelte new file mode 100755 index 0000000..c09c97c --- /dev/null +++ b/src/lib/components/molecules/loader.svelte @@ -0,0 +1,30 @@ + + +
+
+ {#if loader === "normal"} + + {/if} + {#if loader === "sync"} + + {/if} +
+
diff --git a/src/lib/components/molecules/session-validator.svelte b/src/lib/components/molecules/session-validator.svelte new file mode 100755 index 0000000..e73b68f --- /dev/null +++ b/src/lib/components/molecules/session-validator.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..57d643b --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..ef0a953 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..28ecc39 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..f35ac20 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..a235d1f --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..2650ef9 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..edf4840 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..e227219 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7f98004 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/index.ts b/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..be56dd7 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,40 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Trigger = AlertDialogPrimitive.Trigger; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..86827f3 --- /dev/null +++ b/src/lib/components/ui/button/button.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..9cd7a1d --- /dev/null +++ b/src/lib/components/ui/button/index.ts @@ -0,0 +1,54 @@ +import { type VariantProps, tv } from "tailwind-variants"; +import type { Button as ButtonPrimitive } from "bits-ui"; +import Root from "./button.svelte"; + +const buttonVariants = tv({ + base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + variants: { + variant: { + default: "bg-sky-500 text-white hover:bg-sky-600/90", + primaryInverted: "bg-sky-50 text-sky-600 hover:bg-sky-100", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + destructiveInverted: + "bg-destructive/10 text-rose-500 hover:bg-destructive/30", + success: "bg-emerald-500 text-emerald-50 hover:bg-emerald-600/90", + outline: + "border-input bg-background hover:bg-accent hover:text-accent-foreground border", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, +}); + +type Variant = VariantProps["variant"]; +type Size = VariantProps["size"]; + +type Props = ButtonPrimitive.Props & { + variant?: Variant; + size?: Size; +}; + +type Events = ButtonPrimitive.Events; + +export { + Root, + type Props, + type Events, + // + Root as Button, + type Props as ButtonProps, + type Events as ButtonEvents, + buttonVariants, +}; diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..8c35d91 --- /dev/null +++ b/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,35 @@ + + + + + {#if isChecked} + + {:else if isIndeterminate} + + {/if} + + diff --git a/src/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..e7c3ba5 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,36 @@ + + + + + + + + + Close + + + diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..8bc70cc --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..a235d1f --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..6b4448c --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..1d376e4 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-portal.svelte b/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..eb5d0a5 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..06574f3 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..b17ba5e --- /dev/null +++ b/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from "bits-ui"; + +import Title from "./dialog-title.svelte"; +import Portal from "./dialog-portal.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; + +const Root = DialogPrimitive.Root; +const Trigger = DialogPrimitive.Trigger; +const Close = DialogPrimitive.Close; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..75e3bc2 --- /dev/null +++ b/src/lib/components/ui/input/index.ts @@ -0,0 +1,29 @@ +import Root from "./input.svelte"; + +export type FormInputEvent = T & { + currentTarget: EventTarget & HTMLInputElement; +}; +export type InputEvents = { + blur: FormInputEvent; + change: FormInputEvent; + click: FormInputEvent; + focus: FormInputEvent; + focusin: FormInputEvent; + focusout: FormInputEvent; + keydown: FormInputEvent; + keypress: FormInputEvent; + keyup: FormInputEvent; + mouseover: FormInputEvent; + mouseenter: FormInputEvent; + mouseleave: FormInputEvent; + mousemove: FormInputEvent; + paste: FormInputEvent; + input: FormInputEvent; + wheel: FormInputEvent; +}; + +export { + Root, + // + Root as Input, +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..1ff364b --- /dev/null +++ b/src/lib/components/ui/input/input.svelte @@ -0,0 +1,42 @@ + + + diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..8bfca0b --- /dev/null +++ b/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..2a7d479 --- /dev/null +++ b/src/lib/components/ui/label/label.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..14695c8 --- /dev/null +++ b/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/src/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..f2109d6 --- /dev/null +++ b/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..b838270 --- /dev/null +++ b/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..fcb04f6 --- /dev/null +++ b/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..c6c1570 --- /dev/null +++ b/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..49ab7a9 --- /dev/null +++ b/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..a3e59ee --- /dev/null +++ b/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..731c5d5 --- /dev/null +++ b/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..788d4ee --- /dev/null +++ b/src/lib/components/ui/table/table.svelte @@ -0,0 +1,15 @@ + + +
+ + +
+
diff --git a/src/lib/components/ui/textarea/index.ts b/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..6eb6ba3 --- /dev/null +++ b/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,28 @@ +import Root from "./textarea.svelte"; + +type FormTextareaEvent = T & { + currentTarget: EventTarget & HTMLTextAreaElement; +}; + +type TextareaEvents = { + blur: FormTextareaEvent; + change: FormTextareaEvent; + click: FormTextareaEvent; + focus: FormTextareaEvent; + keydown: FormTextareaEvent; + keypress: FormTextareaEvent; + keyup: FormTextareaEvent; + mouseover: FormTextareaEvent; + mouseenter: FormTextareaEvent; + mouseleave: FormTextareaEvent; + paste: FormTextareaEvent; + input: FormTextareaEvent; +}; + +export { + Root, + // + Root as Textarea, + type TextareaEvents, + type FormTextareaEvent, +}; diff --git a/src/lib/components/ui/textarea/textarea.svelte b/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..933e528 --- /dev/null +++ b/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,38 @@ + + + diff --git a/src/lib/server/array.chunk.ts b/src/lib/server/array.chunk.ts new file mode 100755 index 0000000..f6a9993 --- /dev/null +++ b/src/lib/server/array.chunk.ts @@ -0,0 +1,9 @@ +export function chunkArray(array: T[], size: number): T[][] { + const chunked_arr = []; + let index = 0; + while (index < array.length) { + chunked_arr.push(array.slice(index, size + index)); + index += size; + } + return chunked_arr; +} diff --git a/src/lib/server/connectors/redis.ts b/src/lib/server/connectors/redis.ts new file mode 100755 index 0000000..157bda3 --- /dev/null +++ b/src/lib/server/connectors/redis.ts @@ -0,0 +1,10 @@ +import Redis from "ioredis"; + +const redisUrl = process.env.REDIS_URL ?? ""; + +console.log(`\n\n[//] Redis URL: ${redisUrl}`); + +const _redis = + redisUrl && redisUrl.length > 0 ? new Redis(redisUrl) : undefined; + +export const redis = _redis as Redis; diff --git a/src/lib/server/connectors/surreal.db.ts b/src/lib/server/connectors/surreal.db.ts new file mode 100755 index 0000000..edbcbc8 --- /dev/null +++ b/src/lib/server/connectors/surreal.db.ts @@ -0,0 +1,36 @@ +import Surreal from "surrealdb.js"; +export type { QueryResult } from "surrealdb.js/script/types"; + +try { + if (document || window) { + throw new Error("SurrealDB needs a NodeJS environment to run."); + } +} catch (err) {} + +const CONFIG = { + url: process.env.SURREAL_URL ?? "", + user: process.env.SURREAL_USER ?? "", + pass: process.env.SURREAL_PASS ?? "", + ns: process.env.SURREAL_NS ?? "", + db: process.env.SURREAL_DB ?? "", +} as const; + +// for (let key in CONFIG) { +// if ( +// !CONFIG[key as keyof typeof CONFIG] || +// CONFIG[key as keyof typeof CONFIG] === "" +// ) { +// throw new Error(`Missing configuration for ${key}`); +// } +// } + +let _surreal = + CONFIG.url.length > 0 + ? new Surreal(`http://${CONFIG.url}/rpc`, { + auth: { user: CONFIG.user, pass: CONFIG.pass }, + ns: CONFIG.ns, + db: CONFIG.db, + }) + : undefined; + +export const surreal = _surreal as Surreal; diff --git a/src/lib/server/cookie.functions.ts b/src/lib/server/cookie.functions.ts new file mode 100755 index 0000000..215f4a6 --- /dev/null +++ b/src/lib/server/cookie.functions.ts @@ -0,0 +1,50 @@ +import { redis } from "$lib/server/connectors/redis"; + +export type SessionData = { + username: string; + userType: string; +}; + +export const parseCookieString = (cookieString: string) => { + const cookies: Record = cookieString + .split(";") + .reduce((acc, cookie) => { + const [key, value] = cookie.split("="); + if (!key || !value) { + return acc; + } + return { ...acc, [key.trim()]: decodeURIComponent(value) }; + }, {}); + for (const key in cookies) { + if (!key.length || !cookies[key] || cookies[key] === "undefined") { + delete cookies[key]; + } + } + return cookies; +}; + +export const parseSessionData = (sessionData: string) => { + const splits = sessionData.split("|"); + if (splits.length < 5) return false; + return { username: splits[2], userType: splits[3] } as SessionData; +}; + +export const getParsedSession = async (sId: string) => { + const session = await redis.get(sId); + if (!session) { + return false; + } + const parsed = parseSessionData(session); + if (!parsed) { + return false; + } + return parsed; +}; + +export const isSessionValid = async (sId: string) => { + if (!sId || sId.length === 0 || sId.length < 20) { + return false; + } + const session = await redis.get(sId); + return session && session.length > 1; +}; diff --git a/src/lib/server/db/apidata.db.ts b/src/lib/server/db/apidata.db.ts new file mode 100755 index 0000000..73d929a --- /dev/null +++ b/src/lib/server/db/apidata.db.ts @@ -0,0 +1,212 @@ +import type { BookingEntry } from "$lib/utils/data.types"; +import { chunkArray } from "../array.chunk"; +import { surreal } from "../connectors/surreal.db"; + +const getTableName = (date: string) => { + return `apidata${date.replaceAll("-", "")}`; +}; + +const upsertData = async ( + data: BookingEntry[], + date: string, + tries: number = 0, +): Promise => { + const tableName = getTableName(date); + console.log(`[...] Upserting ${data.length} entries into ${tableName}`); + const alreadyPresentIds = new Set(); + try { + const [alreadyPresent] = await surreal.query<[string[]]>( + `select value id from type::table($tableName) where bookDate = $bookDate`, + { tableName, bookDate: date }, + ); + for (let eId of alreadyPresent.result ?? []) { + alreadyPresentIds.add(eId); + } + } catch (err) { + console.log("Failed to fetch, seeing if can try again"); + if (tries >= 3) { + console.log("Max tries exceeded for initial fetch for upserting data"); + return; + } + return await upsertData(data, date, tries++); + } + const oldEntries = [] as any[]; + const newEntries = [] as BookingEntry[]; + for (let entry of data) { + if (!alreadyPresentIds.has(entry.id)) { + newEntries.push({ + ...entry, + id: `${tableName}:${entry.id}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + bookDate: entry.bookDate.split(" ")[0], + requestId: entry.requestId ?? "", + }); + } else { + oldEntries.push({ + distributorId: entry.distributorId, + dealerId: entry.dealerId, + drawId: entry.drawId, + bookDate: entry.bookDate.split(" ")[0], + number: entry.number, + first: entry.first, + second: entry.second, + changedBalance: entry.changedBalance, + sheetName: entry.sheetName, + sheetId: entry.sheetId, + requestId: entry.requestId, + updatedAt: new Date().toISOString(), + }); + } + } + console.log( + `[+] Inserting ${newEntries.length} new entries into ${tableName}`, + ); + + // 5 to 25% of the total data length + let chunkSize = Math.floor( + Math.random() * (data.length * 0.25 - data.length * 0.05) + + data.length * 0.05, + ); + if (chunkSize > 10_000) { + chunkSize = 10_000; + } + + console.log(`Chunk Size : ${chunkSize}`); + + console.log(`[+] Inserting new entries`); + console.time("insertion time"); + const chunks = chunkArray(newEntries, chunkSize).map(async (chunk) => { + await surreal.insert(tableName, chunk); + }); + for (let i = 0; i < chunks.length; i += 2) { + await Promise.all(chunks.slice(i, i + 2)); + } + console.timeEnd("insertion time"); + + console.log( + `[+] Updating ${oldEntries.length} old entries into ${tableName}`, + ); + + const chunks2 = chunkArray(oldEntries, chunkSize).map(async (chunk) => { + await Promise.all( + chunk.map(async (entry) => { + // @ts-ignore + await surreal.update(`${tableName}:${entry.id}`, { + distributorId: entry.distributorId, + dealerId: entry.dealerId, + drawId: entry.drawId, + bookDate: entry.bookDate.split(" ")[0], + number: entry.number, + first: entry.first, + second: entry.second, + changedBalance: entry.changedBalance, + sheetName: entry.sheetName, + sheetId: entry.sheetId, + requestId: entry.requestId, + updatedAt: new Date().toISOString(), + }); + }), + ); + }); + + console.time("update time"); + for (let i = 0; i < chunks2.length; i += 10) { + await Promise.all(chunks2.slice(i, i + 10)); + } + console.timeEnd("update time"); + + console.log( + `[+] Successfully upserted ${data.length} entries into ${tableName}`, + ); +}; + +const getBookingEntriesForDealer = async ( + date: string, + drawId: string, + userId: string, + sorted?: boolean, +) => { + const tableName = getTableName(date); + let query = `select * from type::table($tableName) where bookDate = $date and dealerId = $userId and drawId = $drawId`; + if (sorted) { + query += " order by requestId desc"; + } + const [data] = await surreal.query<[BookingEntry[]]>(query, { + tableName, + date: `${date}`, + userId: parseInt(userId), + drawId: parseInt(drawId), + }); + console.log( + `Found ${JSON.stringify( + data, + )} entries for ${userId}, filters are ${date}, ${drawId} for ${tableName}`, + ); + if (data.status === "OK") { + return data.result ?? []; + } + return []; +}; + +const getBookingEntriesByDraw = async (date: string, drawId: string) => { + const tableName = getTableName(date); + const [data] = await surreal.query<[BookingEntry[]]>( + `select * from type::table($tableName) where bookDate = $date and drawId = $drawId`, + { + tableName, + date: date, + drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId), + }, + ); + if (data.status === "OK") { + return data.result ?? []; + } + return []; +}; + +const deleteDataOlderThan2Weeks = async () => { + const [out] = await surreal.query("info for db"); + // @ts-ignore + const tableNames = Object.keys(out.result.tables); + + const twoWeeksAgo = new Date(); + twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); + + for (const tableName of tableNames) { + if (tableName.startsWith("apidata")) { + const datePart = tableName.slice(7); + const d = new Date( + parseInt(datePart.slice(0, 4), 10), + parseInt(datePart.slice(4, 6), 10) - 1, // Month is 0-based in JavaScript Date + parseInt(datePart.slice(6, 8), 10), + ); + if (d < twoWeeksAgo) { + console.log(`[...] Deleting ${tableName}`); + await surreal.query("remove table if exists " + tableName); + console.log(`[+] Deleted ${tableName}`); + } + } else if (tableName.startsWith("apipostdata_")) { + const datePart = tableName.slice(12); + const d = new Date( + parseInt(datePart.slice(0, 4), 10), + parseInt(datePart.slice(4, 6), 10) - 1, // Month is 0-based in JavaScript Date + parseInt(datePart.slice(6, 8), 10), + ); + if (d < twoWeeksAgo) { + console.log(`[...] Deleting ${tableName}`); + await surreal.query("remove table if exists " + tableName); + console.log(`[+] Deleted ${tableName}`); + } + } else { + console.log(`Skipping ${tableName}`); + } + } +}; + +export const dbApiData = { + upsertData, + getBookingEntriesForDealer, + getBookingEntriesByDraw, + deleteDataOlderThan2Weeks, +}; diff --git a/src/lib/server/db/apidraw.db.ts b/src/lib/server/db/apidraw.db.ts new file mode 100755 index 0000000..0ae8c5e --- /dev/null +++ b/src/lib/server/db/apidraw.db.ts @@ -0,0 +1,100 @@ +import { constants } from "$lib/utils/constants"; +import type { Draw } from "$lib/utils/data.types"; +import { getDraws } from "../external/api.scraping.helpers"; +import { surreal } from "../connectors/surreal.db"; +import { getSessionFromStore } from "../utils/session.service"; + +const tableName = "apidraw"; + +const _populateDrawsTable = async () => { + const session = await getSessionFromStore(constants.SCRAP_API_SESSION_KEY); + if (!session) { + return; + } + const draws = await getDraws(session?.sessionToken); + if (draws.data.length === 0 || !draws.ok) { + return; + } + await surreal.insert( + tableName, + draws.data.map((e) => { + return { + id: e.id, + drawType: e.drawType, + adminId: e.adminId, + title: e.title, + closeTime: e.closeTime, + filterDuplicatesWhilePosting: false, + abRateF: 0, + abcRateF: 0, + abRateS: 0, + abcRateS: 0, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + }), + ); +}; + +const getAllDraws = async ( + skipOptional?: boolean, + retry: number = 0, +): Promise => { + let query = `select * from apidraw order by closeTime`; + const [out] = await surreal.query<[Draw[]]>(query); + if (out.status === "OK") { + const draws = out.result ?? []; + if (draws.length > 0) { + return draws; + } + await _populateDrawsTable(); + if (retry < 3) { + return getAllDraws(skipOptional, retry + 1); + } + } + return []; +}; + +async function setFilterDuplicatesFlag(drawId: string, flag: boolean) { + const [d] = await surreal.select(drawId); + if (!d || !d.id) { + return; + } + console.log("setFilterDuplicatesFlag :: ", drawId, flag); + await surreal.update(drawId, { + ...d, + filterDuplicatesWhilePosting: flag, + updatedAt: new Date().toISOString(), + } as Draw); +} + +async function updateDrawPresetInfo(draw: Draw) { + const drawId = draw.id; + const [d] = await surreal.select(drawId); + if (!d || !d.id) { + return; + } + await surreal.update(drawId, { + ...d, + filterDuplicatesWhilePosting: draw.filterDuplicatesWhilePosting, + abRateF: draw.abRateF, + abcRateF: draw.abcRateF, + abRateS: draw.abRateS, + abcRateS: draw.abcRateS, + updatedAt: new Date().toISOString(), + } as Draw); +} + +const getDraw = async (drawId: string): Promise => { + const draws = await surreal.select( + drawId.includes("apidraw") ? drawId : `apidraw:${drawId}`, + ); + return draws[0]; +}; + +export const dbDraw = { + getAllDraws, + getDraw, + setFilterDuplicatesFlag, + updateDrawPresetInfo, +}; diff --git a/src/lib/server/db/apipostdata.db.ts b/src/lib/server/db/apipostdata.db.ts new file mode 100755 index 0000000..8319deb --- /dev/null +++ b/src/lib/server/db/apipostdata.db.ts @@ -0,0 +1,96 @@ +import type { PostDataEntry, PostDataHistory } from "$lib/utils/data.types"; +import { surreal } from "../connectors/surreal.db"; + +const getTableName = (date: string) => { + return `apipostdata_${date.replaceAll("-", "")}`; +}; + +const upsertData = async (data: PostDataHistory) => { + const tableName = getTableName(data.bookDate); + const [check] = await surreal.query<[PostDataHistory[]]>( + `select * from type::table($tableName) where bookDate = $bookDate and drawId = $drawId`, + { tableName, bookDate: data.bookDate, drawId: data.drawId }, + ); + console.log(check); + const firstOut = check.result ? check.result[0] : undefined; + if (check.status === "OK" && !!firstOut && !!firstOut.id) { + console.log( + `Adding ${data.data.length} entries to ${firstOut.data.length} existing array`, + ); + firstOut.data.push(...data.data); + console.log(`Now have ${firstOut.data.length} entries in data list`); + console.log(`[...] Updating data row in db now`); + await surreal.update(firstOut.id, { + id: firstOut.id, + data: firstOut.data, + drawId: firstOut.drawId, + bookDate: firstOut.bookDate, + updatedAt: new Date().toISOString(), + }); + + return; + } + await surreal.insert(tableName, data); + console.log( + `[+] Inserted post data in ${tableName} for ${data.bookDate} - ${data.drawId}`, + ); +}; + +const getPostDataByDraw = async (date: string, drawId: string) => { + const tableName = getTableName(date); + const [data] = await surreal.query<[PostDataHistory[]]>( + `select * from type::table($tableName) where bookDate = $date and drawId = $drawId`, + { + tableName, + date: date, + drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId), + }, + ); + let out = [] as PostDataEntry[]; + if (data.status === "OK" && data.result.length > 0) { + out = data.result[0].data; + } + return out; +}; + +async function doesPostHistoryDataExist(date: string, drawId: string) { + const tableName = getTableName(date); + const [data] = await surreal.query<[PostDataHistory[]]>( + `select id from type::table($tableName) where bookDate = $date and drawId = $drawId`, + { + tableName, + date: date, + drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId), + }, + ); + + if (data.status === "OK") { + return data.result[0]?.id.length > 0; + } + return false; +} + +async function deletePostDataByDraw(date: string, drawId: string) { + const tableName = getTableName(date); + const [data] = await surreal.query<[PostDataHistory[]]>( + `select id from type::table($tableName) where bookDate = $date and drawId = $drawId`, + { + tableName, + date: date, + drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId), + }, + ); + + if (data.status === "OK") { + await surreal.delete(tableName); + return true; + } + return false; +} + +export const dbApiPostData = { + upsertData, + getPostDataByDraw, + deletePostDataByDraw, + doesPostHistoryDataExist, +}; diff --git a/src/lib/server/db/apiuser.db.ts b/src/lib/server/db/apiuser.db.ts new file mode 100755 index 0000000..34af12e --- /dev/null +++ b/src/lib/server/db/apiuser.db.ts @@ -0,0 +1,279 @@ +import { + type ApiUser, + type LooseApiUser, + type ApiPostUser, + type ApiPostUserWithParent, + ApiUserTypes, + DEFAULT_RANDOM_DISTRIBUTOR, +} from "$lib/utils/data.types"; +import { surreal } from "../connectors/surreal.db"; + +const getUserById = async (userId: string) => { + const query = `select * from apiuser where id = $id`; + const [rizzult] = await surreal.query<[ApiUser[]]>(query, { id: userId }); + return rizzult.result?.[0]; +}; + +const getAllIdsByUserType = async (userType: number) => { + const query = `select value id from apiuser where userType = $userType`; + const rizzult = (await surreal.query<[string[]]>(query, { userType }))[0]; + return (rizzult.result ?? []).map((e) => { + return e.split(":")[1]; + }); +}; + +async function allUsersOfTypeLimitedInfo(userType: number) { + const rizzult = ( + await surreal.query<[ApiPostUser[]]>( + `select id,userName,userId,postData from apiuser where userType = $userType`, + { userType: userType }, + ) + )[0]; + if (rizzult.status == "OK") { + return rizzult.result ?? []; + } + return []; +} + +async function setPostDataFlagForUsers(users: ApiPostUser[]) { + for (const user of users) { + const [u] = await surreal.select(user.id); + if (!u || !u.id) { + continue; + } + await surreal.update(user.id, { + ...u, + postData: user.postData ?? false, + }); + } +} + +const getUserTypeCount = async (userType: number) => { + const queryBase = `select count() from apiuser where userType = $userType`; + let query = `${queryBase} and disable = 0 group all`; + let disabledQuery = `${queryBase} and disable = 1 group all`; + const enabledRizzult = ( + await surreal.query<[{ count: number }[]]>(query, { userType: userType }) + )[0]; + const count = { enabled: 0, disabled: 0 }; + if (enabledRizzult.status == "OK") { + count.enabled = enabledRizzult.result[0]?.count ?? 0; + } + const disabledRizzult = ( + await surreal.query<[{ count: number }[]]>(disabledQuery, { + userType: userType, + }) + )[0]; + if (disabledRizzult.status == "OK") { + count.disabled = disabledRizzult.result[0]?.count ?? 0; + } + return count; +}; + +const allUsersOfType = async (userType: number) => { + const rizzult = ( + await surreal.query<[ApiUser[]]>( + `select * from apiuser where userType = $userType`, + { userType: userType }, + ) + )[0]; + if (rizzult.status == "OK") { + return rizzult.result ?? []; + } + return []; +}; + +async function updatePostUsersBalances( + payload: { balance: number; id: string }[], +) { + console.log("Updating users balances"); + console.log(payload); + for (const each of payload) { + const [rizzult] = await surreal.query<[ApiUser[]]>( + `update $userId set balance = $balance`, + { userId: each.id, balance: each.balance }, + ); + if (rizzult.status !== "OK") { + console.error("updatePostUsersBalance :: ", rizzult); + } + } + console.log("Users balances updated"); +} + +async function getAllPostUsers() { + const [rizzult] = await surreal.query<[ApiPostUser[]]>( + `select id,userName,userId,postData from apiuser where postData = true`, + ); + if (rizzult.status === "OK") { + return rizzult.result ?? []; + } + return []; +} + +async function getAllPostUsersWithParentUsers() { + const [rizzult] = await surreal.query<[ApiPostUserWithParent[]]>( + `select id,userName,userId,postData,parentDistributor,parentAdmin from apiuser where postData = true`, + ); + if (rizzult.status === "OK") { + return rizzult.result ?? []; + } + return []; +} + +const getAllDistributorsWithTheirChildren = async () => { + const distributorIds = await getAllIdsByUserType(ApiUserTypes.DISTRIBUTOR); + const out = distributorIds.map(async (id) => { + const [rizzult] = await surreal.query<[ApiUser[]]>( + `select *, (select * from apiuser where parentDistributor = $id) as children from apiuser where id = $prefixedId`, + { id, prefixedId: `apiuser:${id}` }, + ); + if (rizzult.status == "OK") { + return rizzult.result[0]; + } + return undefined; + }); + const responses = await Promise.all(out); + return responses; +}; + +const getRandomDistributor = async (): Promise => { + const ignoreList = ["001OP9"]; + const randomUser = await _getRandomUser(ApiUserTypes.DISTRIBUTOR, ignoreList); + if (!randomUser) { + return DEFAULT_RANDOM_DISTRIBUTOR; + } + return randomUser as any as ApiUser; +}; + +const getRandomDealer = async (): Promise => { + const ignoreList = ["rizgnore"]; + return _getRandomUser(ApiUserTypes.DEALER, ignoreList); +}; + +const _getRandomUser = async ( + userType: number, + ignoreList: string[], +): Promise => { + console.log("_getRandomUser :: ", userType); + const rizzult = ( + await surreal.query<[ApiUser[]]>( + `select * from apiuser where disable = 0 and userType = $userType and userId notinside $ignoreList order by rand() limit 1`, + { userType: userType, ignoreList: ignoreList }, + ) + )[0]; + if (rizzult.status == "OK") { + return rizzult.result[0]; + } +}; + +const doesExist = async (userId?: string) => { + console.log("doesExist :: ", userId); + if (userId) { + const [rizzult] = await surreal.query<{ count: number }[]>( + "select count() from apiuser where userId = $userId group all", + { userId: userId }, + ); + if (rizzult.status == "OK") { + return rizzult.result?.count > 0; + } + } + return false; +}; + +const insertMany = async (data: LooseApiUser[], postUsers: ApiPostUser[]) => { + console.log("insertMany :: ", data.length); + await surreal.insert( + "apiuser", + data.map((e) => { + return { + ...e, + postData: !!postUsers.find((u) => u.userId === e.userId), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + }), + ); +}; + +async function upsertMany( + data: LooseApiUser[], + wipeTable: boolean, + deleteUserType: typeof ApiUserTypes.DISTRIBUTOR | typeof ApiUserTypes.DEALER, +) { + const postUsers = await getAllPostUsers(); + console.log(postUsers); + if (wipeTable) { + console.log("[wipeTable] :: deleting all previous users"); + await surreal.query("delete from apiuser where userType = $userType", { + userType: deleteUserType, + }); + } + console.log("upsertMany :: ", data.length); + const toCreate = [] as LooseApiUser[]; + const out = data.map(async (apiUser) => { + // INFO: if you do want to keep disabled users, remove this check + if (apiUser.disable === 1) { + return; + } + const [u] = await surreal.select(`apiuser:${apiUser.id}`); + if (!u || !u.id) { + toCreate.push(apiUser); + return; + } + let postData = + u.postData ?? !!postUsers.find((pu) => pu.userId === u.userId) ?? false; + const qId = u.id; + await surreal.update(qId, { + id: u.id, + userId: apiUser.userId, + userType: apiUser.userType, + disableBooking: apiUser.disableBooking, + sendVoucher: apiUser.sendVoucher, + voucherGenerated: apiUser.voucherGenerated, + parentAdmin: apiUser.parentAdmin, + parentDistributor: apiUser.parentDistributor, + userName: apiUser.userName, + userCity: apiUser.userCity, + password: apiUser.password, + accessDenied: apiUser.accessDenied, + phoneNumber: apiUser.phoneNumber, + emailAddress: apiUser.emailAddress, + disable: apiUser.disable, + commission: apiUser.commission, + commissionPangora: apiUser.commissionPangora, + allowTitles: apiUser.allowTitles, + specialDealer: apiUser.specialDealer, + allowBalance: apiUser.allowBalance, + balance: apiUser.balance, + profitlossShare: apiUser.profitlossShare, + shareProfitonly: apiUser.shareProfitonly, + allowRemoveold: apiUser.allowRemoveold, + removeDays: apiUser.removeDays, + language: apiUser.language, + postData, + createdAt: u.createdAt, + updatedAt: new Date().toISOString(), + }); + }); + await Promise.allSettled(out); + if (toCreate.length > 0) { + await insertMany(toCreate, postUsers); + } +} + +export const dbApiUser = { + allUsersOfType, + allUsersOfTypeLimitedInfo, + getUserById, + getAllDistributorsWithTheirChildren, + getUserTypeCount, + getAllIdsByUserType, + getAllPostUsers, + getAllPostUsersWithParentUsers, + getRandomDistributor, + getRandomDealer, + doesExist, + upsertMany, + setPostDataFlagForUsers, + updatePostUsersBalances, +}; diff --git a/src/lib/server/db/booking.db.ts b/src/lib/server/db/booking.db.ts new file mode 100755 index 0000000..b5ea1fe --- /dev/null +++ b/src/lib/server/db/booking.db.ts @@ -0,0 +1,4 @@ + + +export const dbBooking = { +}; diff --git a/src/lib/server/db/finalsheet.db.ts b/src/lib/server/db/finalsheet.db.ts new file mode 100755 index 0000000..0470432 --- /dev/null +++ b/src/lib/server/db/finalsheet.db.ts @@ -0,0 +1,40 @@ +import type { FinalSheetData } from "$lib/utils/data.types"; +import { surreal } from "../connectors/surreal.db"; + +const getTableName = (date: string) => { + return `finalsheet${date.replaceAll("-", "")}`; +}; + +const upsertData = async (data: FinalSheetData, date: string) => { + const tableName = getTableName(date); + const [present] = await surreal.query<[FinalSheetData[]]>( + `select id from type::table($tableName) where date = $date and drawId = $drawId`, + { tableName, date: `${date}`, drawId: data.drawId } + ); + if (present) { + // @ts-ignore + await surreal.update(`${tableName}:${data.id}`, { + date: data.date, + drawId: data.drawId, + data: data.data, + totals: data.totals, + // @ts-ignore + createdAt: present?.result[0]?.createdAt ?? new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } else { + // @ts-ignore + await surreal.create(`${tableName}:${data.id}`, { + date: data.date, + drawId: data.drawId, + data: data.data, + totals: data.totals, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } +}; + +export const dbFinalSheet = { + upsertData, +}; diff --git a/src/lib/server/db/presetdata.db.ts b/src/lib/server/db/presetdata.db.ts new file mode 100755 index 0000000..47dde5e --- /dev/null +++ b/src/lib/server/db/presetdata.db.ts @@ -0,0 +1,65 @@ +import type { + ApiPostUser, + PostDataEntry, + PresetDataEntry, +} from "$lib/utils/data.types"; +import { surreal } from "../connectors/surreal.db"; + +const getTableName = (date: string) => { + return `presetdata_${date.replaceAll("-", "")}`; +}; + +const insertData = async (data: PresetDataEntry[]) => { + if (data.length < 1) return; + const tableName = getTableName(data[0].bookDate); + const out = await surreal.insert(tableName, data); + console.log( + `[+] Inserted post data in ${tableName} for ${data[0].bookDate} - ${data[0].drawId}`, + ); + return out; +}; + +const getDataByDraw = async (date: string, drawId: number) => { + const tableName = getTableName(date); + const [data] = await surreal.query<[PresetDataEntry[]]>( + `select * from type::table($tableName) where bookDate = $date and drawId = $drawId`, + { tableName, date, drawId }, + ); + return data.result || ([] as PresetDataEntry[]); +}; + +const getDataGroupedBySheetByDraw = async (date: string, drawId: number) => { + const tableName = getTableName(date); + const [data] = await surreal.query<[PresetDataEntry[]]>( + `select * from type::table($tableName) where bookDate = $date and drawId = $drawId`, + { tableName, date, drawId }, + ); + const out = { + abData: [] as PresetDataEntry[], + abcData: [] as PresetDataEntry[], + all: data.result || ([] as PresetDataEntry[]), + }; + for (const row of data.result ?? []) { + if (row.number.length === 2) { + out.abData.push(row); + } else if (row.number.length === 3) { + out.abcData.push(row); + } + } + return out; +}; + +async function deleteDataByIds(date: string, ids: string[]) { + const tableName = getTableName(date); + await surreal.query<[PresetDataEntry[]]>( + `delete from type::table($tableName) where id in $ids`, + { tableName, ids }, + ); +} + +export const dbPresetData = { + insertData, + getDataGroupedBySheetByDraw, + getDataByDraw, + deleteDataByIds, +}; diff --git a/src/lib/server/db/user.db.ts b/src/lib/server/db/user.db.ts new file mode 100755 index 0000000..8ea842a --- /dev/null +++ b/src/lib/server/db/user.db.ts @@ -0,0 +1,90 @@ +import type { User } from "$lib/utils/data.types"; +import { surreal, type QueryResult } from "../connectors/surreal.db"; + +export const dbUser = { + doesExist: async (username?: string) => { + if (username) { + const [rizzult] = await surreal.query<{ count: number }[]>( + "select count() from user where username = $username group all", + { username: username } + ); + if (rizzult.status == "OK") { + return rizzult.result?.count > 0; + } + } + return false; + }, + create: async (data: { + username: string; + password: string; + userType: string; + association: string; + }) => { + const doesUserAlreadyExist = await dbUser.doesExist(data.username); + console.log("doesUserAlreadyExist :: ", doesUserAlreadyExist); + if (doesUserAlreadyExist) { + return [{ message: "User already exists." }]; + } + const { username, password, association, userType } = data; + const out = await surreal.create(`user:ulid()`, { + createdAt: Date.now().toString(), + updatedAt: Date.now().toString(), + username, + password, + userType, + association, + }); + return out as User[]; + }, + all: async () => { + return await surreal.select("user"); + }, + get: async (d: { + username?: string; + id?: string; + }): Promise => { + if (d.id) { + return (await surreal.select(`user:${d.id}`))[0]; + } + if (d.username) { + const rizzult = ( + await surreal.query<[User[]]>( + `select * from user where username = $username`, + { username: d.username } + ) + )[0]; + if (rizzult.status == "OK") { + return rizzult.result[0]; + } + } + return undefined; + }, + getChildren: async (username?: string) => { + const rizzult = await surreal.query( + `select * from user where association = $username`, + { username: username } + ); + return getParsedUsers(rizzult); + }, + update: async (id: string, data: { association: string }) => { + const [rizzult] = await surreal.update(`user:${id}`, { + updatedAt: Date.now().toString(), + association: data.association, + } as User); + return rizzult; + }, + delete: async (id: string) => { + const out = await surreal.delete(`user:${id}`); + return out[0].id; + }, +}; + +const getParsedUsers = (data: QueryResult[]) => { + const users = [] as User[]; + for (const each of data) { + if (each.status == "OK") { + users.push(each.result); + } + } + return users; +}; diff --git a/src/lib/server/deprecated.fs.hlprz.ts b/src/lib/server/deprecated.fs.hlprz.ts new file mode 100755 index 0000000..2bdbf3a --- /dev/null +++ b/src/lib/server/deprecated.fs.hlprz.ts @@ -0,0 +1,291 @@ +import { + COMMISSION_PERCENTAGE, + LEXICODE_MATHCER_PATTERNS, + LEXICODE_PRIZE_PERCENTAGES, + NUMBERS_IN_FIRST_DRAW, + NUMBERS_IN_SECOND_DRAW, +} from "$lib/utils/constants"; +import type { + BookingEntry, + FinalSheetData, + FinalSheetRow, + LexiCodeCacheObject, + ReducedFinalSheetData, + ServerError, +} from "$lib/utils/data.types"; +import { dbApiData } from "./db/apidata.db"; +import { redis } from "./connectors/redis"; +import { getULID, getUUID } from "$lib/utils"; + +export const getCompiledFinalSheet = async (date: string, drawId: string) => { + const bookingEntries = await dbApiData.getBookingEntriesByDraw(date, drawId); + const finalSheet = { + id: getULID(), + date, + drawId, + data: [], + totals: getDefaultTotals(), + } as FinalSheetData; + const lexiCodeCache = await getLexiCodeCache(); + const { sheetCache, failed } = await getSheetCache(bookingEntries); + if (failed.length > 0) { + console.log(`[-] Failed to find lexicodes for ${failed.length} entries`); + } + console.log("[...] Now compiling the final sheet"); + for (const number of get4DigitGenerator()) { + const fsRow = { + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + profit: { first: 0, second: 0 }, + } as FinalSheetRow; + const childNumbers = lexiCodeCache[number]; + let vals = getLexiCodeCacheObject(number); + const sheetCacheKeys = Object.keys(sheetCache); + for (const child of childNumbers) { + const lco = getLexiCodeCacheObject(child.number); + if (sheetCacheKeys.includes(child.number)) { + lco.number = child.number; + // TODO: make this into a loop for first and second + vals.frequency.first += sheetCache[child.number].frequency.first; + vals.rate.first += sheetCache[child.number].rate.first; + vals.prize.first += sheetCache[child.number].prize.first; + lco.frequency.first += sheetCache[child.number].frequency.first; + lco.rate.first += sheetCache[child.number].rate.first; + lco.prize.first += sheetCache[child.number].prize.first; + + vals.frequency.second += sheetCache[child.number].frequency.second; + vals.rate.second += sheetCache[child.number].rate.second; + vals.prize.second += sheetCache[child.number].prize.second; + lco.frequency.second += sheetCache[child.number].frequency.second; + lco.rate.second += sheetCache[child.number].rate.second; + lco.prize.second += sheetCache[child.number].prize.second; + } + // @ts-ignore + fsRow[child.lexiCode] = lco; + } + fsRow.id = getUUID(); + fsRow.number = number; + fsRow.frequency = vals.frequency; + fsRow.rate = { first: vals.rate.first, second: vals.rate.second }; + // TODO: MAYBE: recalculate these + fsRow.prize = { first: vals.prize.first, second: vals.prize.second }; + fsRow.profit = { + first: calculateProfit(vals.rate.first, vals.prize.first), + second: calculateProfit(vals.rate.second, vals.prize.second), + }; + finalSheet.data.push(fsRow); + } + console.log("[+] Final sheet compilation complete"); + return { finalSheet, ok: true, errors: [] as ServerError }; +}; + +const getSheetCache = async (bookingEntries: BookingEntry[]) => { + const sheetCache = {} as Record; + const failed = [] as BookingEntry[]; + console.log("[...] Preparing the FS final sheet cache"); + // INFO: first loop we calculate the vals for each indiv. number of the first and second + for (const entry of bookingEntries) { + const no = entry.number; + const lexiCode = getLexiCode(no); + if (!lexiCode) { + failed.push(entry); + continue; + } + if (sheetCache[no] === undefined) { + sheetCache[no] = getLexiCodeCacheObject(no); + } + sheetCache[no].frequency.first++; + sheetCache[no].rate.first += entry.first; + sheetCache[no].prize.first += calculatePrize( + getRateAfterCommission(sheetCache[no].rate.first), + lexiCode, + "first", + getNoOfDigits(lexiCode), + NUMBERS_IN_FIRST_DRAW + ); + sheetCache[no].frequency.second++; + sheetCache[no].rate.second += entry.second; + sheetCache[no].prize.second += calculatePrize( + getRateAfterCommission(sheetCache[no].rate.second), + lexiCode, + "second", + getNoOfDigits(lexiCode), + NUMBERS_IN_SECOND_DRAW + ); + } + return { sheetCache, failed }; +}; + +const getDefaultTotals = () => { + return { + commission: { first: 0, second: 0 }, + netRate: { first: 0, second: 0 }, + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + profit: { first: 0, second: 0 }, + frequency: { first: 0, second: 0 }, + }; +}; + +const getNoOfDigits = (lexiCode: string) => { + const lens = { 1: 10, 2: 100, 3: 1000, 4: 10000 }; + return lens[lexiCode.replaceAll("+", "").length as keyof typeof lens]; +}; + +const getRateAfterCommission = (rate: number) => { + return rate - rate * COMMISSION_PERCENTAGE; +}; + +const getLexiCodeCache = async () => { + type CacheType = Record; + const rKey = "lexicodecache"; + const found = await redis.get(rKey); + if (found) { + return JSON.parse(found) as CacheType; + } + const cache = {} as CacheType; + for (const number of get4DigitGenerator()) { + cache[number] = getAllMatchingChildNumbers(number); + } + await redis.set(rKey, JSON.stringify(cache)); + return cache; +}; + +const getAllMatchingChildNumbers = (parent: string) => { + const out = [] as { number: string; lexiCode: string }[]; + out.push({ number: `${parent[0]}`, lexiCode: "a" }); + out.push({ number: `+${parent[0]}`, lexiCode: "+a" }); + out.push({ number: `++${parent[0]}`, lexiCode: "++a" }); + out.push({ number: `+++${parent[0]}`, lexiCode: "+++a" }); + out.push({ number: `${parent[0]}${parent[1]}`, lexiCode: "ab" }); + out.push({ number: `+${parent[0]}${parent[1]}`, lexiCode: "+ab" }); + out.push({ number: `${parent[0]}+${parent[1]}`, lexiCode: "a+b" }); + out.push({ number: `+${parent[0]}+${parent[1]}`, lexiCode: "+a+b" }); + out.push({ number: `++${parent[0]}${parent[1]}`, lexiCode: "++ab" }); + out.push({ number: `${parent[0]}++${parent[1]}`, lexiCode: "a++b" }); + out.push({ number: `${parent[0]}${parent[1]}${parent[2]}`, lexiCode: "abc" }); + out.push({ + number: `+${parent[0]}${parent[1]}${parent[2]}`, + lexiCode: "+abc", + }); + out.push({ + number: `${parent[0]}+${parent[1]}${parent[2]}`, + lexiCode: "a+bc", + }); + out.push({ + number: `${parent[0]}${parent[1]}+${parent[2]}`, + lexiCode: "ab+c", + }); + out.push({ number: parent, lexiCode: "abcd" }); + return out; +}; + +function* get4DigitGenerator(): Generator { + for (let i = 0; i < 10; i++) { + for (let j = 0; j < 10; j++) { + for (let k = 0; k < 10; k++) { + for (let l = 0; l < 10; l++) { + yield `${i}${j}${k}${l}`; + } + } + } + } +} + +const calculatePrize = ( + amount: number, + lexiCode: string, + type: "first" | "second", + noOfDigits: number, + noOfDrawNumbers: number +) => { + const lexiCodePercentage = + LEXICODE_PRIZE_PERCENTAGES[type][ + lexiCode as keyof typeof LEXICODE_PRIZE_PERCENTAGES.first + ]; + if (amount && lexiCodePercentage > 0 && noOfDrawNumbers > 0) { + return Number( + ( + (amount * noOfDigits * (lexiCodePercentage / 100)) / + noOfDrawNumbers + ).toFixed(2) + ); + } + return 0; +}; + +const calculateProfit = (rate: number, prize: number) => { + return getRateAfterCommission(rate) - prize; +}; + +const getLexiCode = (no: string) => { + for (const [lexicode, pattern] of Object.entries(LEXICODE_MATHCER_PATTERNS)) { + if (pattern.test(no)) { + return lexicode; + } + } + return false; +}; + +const getLexiCodeCacheObject = (no: string) => { + return { + number: no, + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + frequency: { first: 0, second: 0 }, + } as LexiCodeCacheObject; +}; + +// const getMatchingParents = (no: string) => { +// const matching = [] as string[]; +// for (const parent of get4DigitGenerator()) { +// if (doesNumberMatch(parent, no)) { +// matching.push(parent); +// } +// } +// return matching; +// }; + +// const doesNumberMatch = (parent: string, child: string) => { +// let allMatch = true; +// for (let i = 0; i < child.length; i++) { +// if (parent[i] !== child[i] && child[i] !== "+") { +// allMatch = false; +// } +// } +// return allMatch; +// }; + +// const getLexiCodeCache = async () => { +// const rKey = "lexicodecache"; +// const keyCount = await redis.keys(rKey); +// if (keyCount.length === 1) { +// return JSON.parse((await redis.get(rKey)) ?? ""); +// } +// const lexicodeCache: Record = {}; +// for (let i = 0; i < 10; i++) { +// lexicodeCache[i.toString()] = getMatchingParents(i.toString()); +// lexicodeCache[`+${i}`] = getMatchingParents(`+${i}`); +// lexicodeCache[`++${i}`] = getMatchingParents(`++${i}`); +// lexicodeCache[`+++${i}`] = getMatchingParents(`+++${i}`); +// for (let j = 0; j < 10; j++) { +// lexicodeCache[`${i}${j}`] = getMatchingParents(`${i}${j}`); +// lexicodeCache[`+${i}${j}`] = getMatchingParents(`+${i}${j}`); +// lexicodeCache[`${i}+${j}`] = getMatchingParents(`${i}+${j}`); +// lexicodeCache[`+${i}+${j}`] = getMatchingParents(`+${i}+${j}`); +// lexicodeCache[`++${i}${j}`] = getMatchingParents(`++${i}${j}`); +// lexicodeCache[`${i}++${j}`] = getMatchingParents(`${i}++${j}`); +// for (let k = 0; k < 10; k++) { +// lexicodeCache[`${i}${j}${k}`] = getMatchingParents(`${i}${j}${k}`); +// lexicodeCache[`+${i}${j}${k}`] = getMatchingParents(`+${i}${j}${k}`); +// lexicodeCache[`${i}+${j}${k}`] = getMatchingParents(`${i}+${j}${k}`); +// lexicodeCache[`${i}${j}+${k}`] = getMatchingParents(`${i}${j}+${k}`); +// for (let l = 0; l < 10; l++) { +// lexicodeCache[`${i}${j}${k}${l}`] = [`${i}${j}${k}${l}`]; +// } +// } +// } +// } +// await redis.set(rKey, JSON.stringify(lexicodeCache)); +// return lexicodeCache; +// }; diff --git a/src/lib/server/external/api.scraping.helpers.ts b/src/lib/server/external/api.scraping.helpers.ts new file mode 100755 index 0000000..597fedc --- /dev/null +++ b/src/lib/server/external/api.scraping.helpers.ts @@ -0,0 +1,316 @@ +import { getRandomUserAgent, getULID, sleep } from "$lib/utils"; +import { constants } from "$lib/utils/constants"; +import type { BookingEntry, Draw, LooseApiUser } from "$lib/utils/data.types"; +import { rng } from "$lib/utils/rng"; + +export const testIfSessionIsValid = async (jwt: string) => { + try { + const res = await fetch( + `${constants.SCRAP_API_URL}/v1/user/get-balance?userId=6339`, + { + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + Authorization: jwt, + "User-Agent": getRandomUserAgent(), + }, + }, + ); + if (res.status !== 200) { + return false; + } + const rj = (await res.json()) as { + code: number; + success: boolean; + message: string; + data: any; + time: string; + }; + return rj.code == 200 && rj.success; + } catch (err) { + console.log(err); + return false; + } +}; + +export const getSessionToken = async (payload: { + userId: string; + password: string; + verifyToken: string; + code: string; + userType: number; +}): Promise<{ ok: boolean; message: string }> => { + console.log("Requesting..."); + const res = await fetch(`${constants.SCRAP_API_URL}/v1/auth/login`, { + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + "User-Agent": getRandomUserAgent(), + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + const out = await res.json(); + if (out.code !== 200) { + return { ok: false, message: out.message }; + } + return { ok: true, message: out.data.token }; +}; + +export async function getUsersBalance(userId: number, jwt: string) { + try { + const res = await fetch( + `${constants.SCRAP_API_URL}/v1/user/get-balance?userId=${userId}`, + { + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + Authorization: jwt, + "User-Agent": getRandomUserAgent(), + }, + }, + ); + const rj = (await res.json()) as { + code: number; + success: boolean; + message: string; + data: { allowedBalance: number; balance: number }; + time: string; + }; + if (res.status !== 200 || rj.code !== 200 || !rj.success) { + console.log( + `[!] Error getting balance for ${userId} :: ${JSON.stringify(rj)}`, + ); + return false; + } + return rj.data.balance; + } catch (err) { + console.log(err); + return false; + } +} + +export const getDealers = async (jwt: string, distributor_ids: string[]) => { + try { + // // Create an array of promises for each fetch request + const requests = distributor_ids.map(async (did) => { + await sleep(rng(100, 10000)); + const res = await fetch( + `${constants.SCRAP_API_URL}/v1/user/dealer-list`, + { + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + "Content-Type": "application/json", + "User-Agent": getRandomUserAgent(), + Authorization: jwt, + }, + body: JSON.stringify({ + page: 1, + pageSize: 999999, + parentDistributor: parseInt(did), + }), + }, + ); + const data = (await res.json()) as { + code: number; + success: boolean; + message: string; + data: { + items: any[]; + total: number; + }; + }; + if (data.code !== 200 || !data.success) { + return { + dealers: [], + ok: false, + code: data.code, + message: data.message, + }; + } + const dealers = data.data.items.map((item) => item.dealer); + return { + dealers, + ok: res.status === 200 && data.success, + code: data.code, + message: data.message, + }; + }); + // // Wait for all promises to resolve + const responses = await Promise.all(requests); + const dealers: LooseApiUser[] = []; + const errors: { message: string }[] = []; + for (const res of responses) { + if (res.code !== 200 || !res.ok) { + errors.push({ message: res.message }); + continue; + } + for (const dealer of res.dealers) { + dealers.push(dealer); + } + } + + // fs.writeFileSync("dealers.json", JSON.stringify(dealers, null, 2)); + + return { dealers, errors }; + } catch (err) { + console.error(err); + return { + dealers: [], + errors: [{ message: "An error occured during fetching dealers" }], + }; + } +}; + +export const getDistributors = async (jwt: string) => { + const res = await fetch( + `${constants.SCRAP_API_URL}/v1/user/distributor-list`, + { + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + Authorization: jwt, + "Content-Type": "application/json", + "User-Agent": getRandomUserAgent(), + }, + body: JSON.stringify({ + page: 1, + pageSize: 999999, + parentDistributor: 15, + }), + }, + ); + const json = (await res.json()) as { + code: number; + success: boolean; + message: string; + data: { total: number; items: any[] }; + }; + + if (!json.data.items || json.code !== 200 || !json.success) { + return { ok: false, message: json.message, data: [] }; + } + + // fs.writeFileSync( + // "distributors.json", + // JSON.stringify(json.data.items, null, 2), + // ); + + return { + ok: true, + message: "", + data: json.data.items.map((item) => item.distributor), + }; +}; + +export const getDraws = async (jwt: string) => { + const res = await fetch( + `${constants.SCRAP_API_URL}/v1/draw/list-my?userId=15`, + { + method: "GET", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + Authorization: jwt, + "Content-Type": "application/json", + "User-Agent": getRandomUserAgent(), + }, + }, + ); + type J = { + code: number; + success: boolean; + message: string; + data: { draw: Draw }[]; + }; + let decoded = (await res.json()) as { data: J }; + const json = (decoded.data.success ? decoded.data : decoded) as any as J; + if (json.code !== 200 || !json.success || !json.data) { + return { ok: false, message: json.message, data: [] }; + } + return { + ok: true, + message: "", + data: json.data.map((item) => item.draw), + }; +}; + +export const getData = async ( + jwt: string, + userIds: number[], + drawId: number, + chosenDate: string, +) => { + const res = await fetch(`${constants.SCRAP_API_URL}/v1/book/list2`, { + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + Authorization: jwt, + "Content-Type": "application/json", + "User-Agent": getRandomUserAgent(), + }, + body: JSON.stringify({ + userType: 3, + userIds, + drawId: drawId, + startDate: chosenDate, + endDate: chosenDate, + beAdmin: false, + containImported: false, + keyword: "", + }), + }); + type J = { + code: number; + success: boolean; + message: string; + data: { book: BookingEntry; user: any }[]; + }; + let decoded = (await res.json()) as { data: J }; + const json = (decoded.data.success ? decoded.data : decoded) as any as J; + if (json.code !== 200 || !json.success || !json.data) { + return { ok: false, message: json.message, data: [] }; + } + return { ok: true, message: "", data: json.data.map((e) => e.book) }; +}; + +export const mockGetUserData = async ( + jwt: string, + userIds: number[], + drawId: number, + chosenDate: string, +) => { + console.log("Rizzzzard of Mogwards!"); + const entries = [] as BookingEntry[]; + + const rng = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + const randomCeil = rng(10_000, 200_000); + + await sleep(rng(100, 1000)); + + for (let i = 0; i < randomCeil; i++) { + const _f = rng(5, 50); + const _s = rng(5, 50); + const f = _f - (_f % 5); + const s = _s - (_s % 5); + + entries.push({ + id: getULID(), + bookDate: chosenDate, + changedBalance: f + s, + first: f, + second: s, + dealerId: userIds[rng(0, userIds.length - 1)], + distributorId: 6339, + drawId: drawId, + number: rng(0, 9999).toString(), + requestId: new Date().getTime().toString(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + sheetId: "0", + sheetName: "", + }); + } + + return { ok: true, message: "", data: entries }; +}; diff --git a/src/lib/server/finalsheet.helpers.ts b/src/lib/server/finalsheet.helpers.ts new file mode 100755 index 0000000..b00ba75 --- /dev/null +++ b/src/lib/server/finalsheet.helpers.ts @@ -0,0 +1,270 @@ +import type { + BookingEntry, + FSTotals, + FinalSheetRow, + LexiCodeCacheObject, + ReducedFinalSheetData, + ReducedFinalSheetRow, + ServerError, +} from "$lib/utils/data.types"; +import { dbApiData } from "./db/apidata.db"; +import { redis } from "./connectors/redis"; +import { getDefaultTotals, getULID } from "$lib/utils"; +import { + calculatePrize, + calculateProfit, + get4DigitGenerator, + getAllMatchingChildNumbers, + getCommisionAmt, + getLexiCode, + getLexiCodeCacheObject, + getNoOfDigits, + getNetRate, +} from "$lib/utils/finalsheet.utils"; + +export const getReducedFinalSheet = async (fsData: ReducedFinalSheetData) => { + const bookingEntries = await dbApiData.getBookingEntriesByDraw( + fsData.date, + fsData.drawId + ); + console.log( + `[...] Got ${bookingEntries.length} booking entries for ${fsData.date}, draw ${fsData.drawId}` + ); + const lexiCodeCache = await getLexiCodeCache(); + const { sheetCache, totals, failed } = await getSheetCache(bookingEntries); + const sheetCacheKeys = new Set(); + for (const each of Object.keys(sheetCache)) { + sheetCacheKeys.add(each); + } + if (failed.length > 0) { + console.log(`[-] Failed to find lexicodes for ${failed.length} entries`); + console.log(failed.map((e) => e.number)); + } + fsData.totals = totals; + console.log("[...] Compiling the final sheet"); + // fs.writeFileSync("test.json", JSON.stringify(sheetCache, null, 2)); + let highestRate = { first: 0, second: 0 }; + for (const number of get4DigitGenerator()) { + const fsRow = { + id: getULID(), + number: number, + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + profit: { first: 0, second: 0 }, + frequency: { first: 0, second: 0 }, + frequencies: { abcd: { first: 0, second: 0 } }, + } as ReducedFinalSheetRow; + const childNumbers = lexiCodeCache[number]; + let vals = getLexiCodeCacheObject(number); + + for (const child of childNumbers) { + if (!sheetCacheKeys.has(child.number)) { + continue; + } + for (const each of ["first", "second"] as const) { + vals.frequency[each] += sheetCache[child.number].frequency[each]; + vals.rate[each] += sheetCache[child.number].rate[each]; + vals.prize[each] += sheetCache[child.number].prize[each]; + if (child.lexiCode !== "abcd") { + continue; + } + fsRow.frequencies[child.lexiCode][each] = + sheetCache[child.number].frequency[each]; + } + } + + for (const each of ["first", "second"] as const) { + fsRow.frequency[each] = vals.frequency[each]; + fsRow.rate[each] = vals.rate[each]; + fsRow.prize[each] = vals.prize[each]; + fsRow.profit[each] = calculateProfit( + totals.netRate[each], + vals.prize[each] + ); + } + if (fsRow.rate.first > highestRate.first) { + highestRate.first = fsRow.rate.first; + } + if (fsRow.rate.second > highestRate.second) { + highestRate.second = fsRow.rate.second; + } + // console.log(fsRow); + // throw new Error("test"); + fsData.data.push(fsRow); + } + console.log("[+] Reduced Final sheet compilation complete"); + return { ok: true, errors: [] as ServerError }; +}; + +export const getTargetFSRow = async ( + drawId: string, + date: string, + number: string +) => { + const bookingEntries = await dbApiData.getBookingEntriesByDraw(date, drawId); + const lexiCodeCache = await getLexiCodeCache(); + const { sheetCache, totals, failed } = await getSheetCache(bookingEntries); + if (failed.length > 0) { + console.log(`[-] Failed to find lexicodes for ${failed.length} entries`); + console.log(failed.map((e) => e.number)); + } + console.log("[...] Preparing the FS row"); + // fs.writeFileSync("test.json", JSON.stringify(sheetCache, null, 2)); + let highestRate = { first: 0, second: 0 }; + const fsRow = { + id: getULID(), + number: number, + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + profit: { first: 0, second: 0 }, + frequency: { first: 0, second: 0 }, + } as FinalSheetRow; + const childNumbers = lexiCodeCache[number]; + let vals = getLexiCodeCacheObject(number); + const sheetCacheKeys = Object.keys(sheetCache); + for (const child of childNumbers) { + if (sheetCacheKeys.includes(child.number)) { + for (const each of ["first", "second"] as const) { + vals.frequency[each] += sheetCache[child.number].frequency[each]; + vals.rate[each] += sheetCache[child.number].rate[each]; + vals.prize[each] += sheetCache[child.number].prize[each]; + } + } + } + for (const each of ["first", "second"] as const) { + fsRow.frequency[each] = vals.frequency[each]; + fsRow.rate[each] = vals.rate[each]; + fsRow.prize[each] = vals.prize[each]; + fsRow.profit[each] = calculateProfit( + totals.netRate[each], + vals.prize[each] + ); + } + if (fsRow.rate.first > highestRate.first) { + highestRate.first = fsRow.rate.first; + } + if (fsRow.rate.second > highestRate.second) { + highestRate.second = fsRow.rate.second; + } + console.log("[+] FS Row prepared"); + return { ok: true, errors: [] as ServerError, data: fsRow }; +}; + +const getSheetCache = async (bookingEntries: BookingEntry[]) => { + const sheetCache = {} as Record; + const totals = getDefaultTotals() as FSTotals; + const failed = [] as BookingEntry[]; + console.log("[...] Preparing the pre-FS cache"); + // INFO: first loop we calculate the vals for each indiv. number of the first and second + for (const entry of bookingEntries) { + const no = entry.number; + const lexiCode = getLexiCode(no); + if (!lexiCode) { + failed.push(entry); + continue; + } + if (sheetCache[no] === undefined) { + sheetCache[no] = getLexiCodeCacheObject(no); + } + for (const each of ["first", "second"] as const) { + if (entry[each] > 0) { + sheetCache[no].frequency[each]++; + } + sheetCache[no].rate[each] += entry[each]; + sheetCache[no].prize[each] = calculatePrize( + sheetCache[no].rate[each], + lexiCode, + each, + getNoOfDigits(lexiCode) + ); + } + } + if (bookingEntries.length > 0) { + for (const value of Object.values(sheetCache)) { + for (const each of ["first", "second"] as const) { + totals.frequency[each] += value.frequency[each]; + totals.prize[each] += value.prize[each]; + totals.rate[each] += value.rate[each]; + } + } + for (const each of ["first", "second"] as const) { + totals.commission[each] = getCommisionAmt(totals.rate[each]); + totals.netRate[each] = getNetRate(totals.rate[each]); + } + } + return { sheetCache, failed, totals }; +}; + +const getLexiCodeCache = async () => { + type CacheType = Record; + const rKey = "lexicodecache"; + const found = await redis.get(rKey); + if (found) { + return JSON.parse(found) as CacheType; + } + const cache = {} as CacheType; + for (const number of get4DigitGenerator()) { + cache[number] = getAllMatchingChildNumbers(number); + } + await redis.setex(rKey, 3600 * 24, JSON.stringify(cache)); + return cache; +}; + +const getCachedReducedFinalSheet = async (date: string, drawId: string) => { + const key = `cfinalsheet:${date}:${drawId}`; + const cached = await redis.get(key); + if (cached) { + return JSON.parse(cached); + } + return null; +}; + +const setCachedReducedFinalSheet = async ( + date: string, + drawId: string, + data: ReducedFinalSheetData +) => { + const key = `cfinalsheet:${date}:${drawId}`; + await redis.setex(key, 3600 * 24, JSON.stringify(data)); +}; + +export const deleteCachedReducedFinalSheet = async ( + date: string, + drawId: string +) => { + await redis.del(`cfinalsheet:${date}:${drawId}`); +}; + +export const getTestBookingData = async (drawId: string, date: string) => { + const numbers = {} as Record>; + for (const each of get4DigitGenerator()) { + const childNumbers = getAllMatchingChildNumbers(each); + for (const child of childNumbers) { + if (numbers[child.lexiCode] === undefined) { + numbers[child.lexiCode] = new Set(); + } + numbers[child.lexiCode].add(child.number); + } + } + const out = [] as BookingEntry[]; + for (const v of Object.values(numbers)) { + for (const child of v) { + const entry = { + id: getULID(), + drawId: Number(drawId.split(":")[1]), + bookDate: date, + date, + number: child, + first: 10, + second: 10, + dealerId: 4677, + sheetId: "test", + requestId: getULID(), + distributorId: 6339, + changedBalance: 0, + } as BookingEntry; + out.push(entry); + } + } + return out; +}; diff --git a/src/lib/server/hashing.ts b/src/lib/server/hashing.ts new file mode 100755 index 0000000..15dc912 --- /dev/null +++ b/src/lib/server/hashing.ts @@ -0,0 +1,11 @@ +import bcrypt from "bcryptjs"; + +const rounds = 10; + +export const hashData = (data: string): string => { + return bcrypt.hashSync(data, rounds); +}; + +export const compareData = (data: string, hash: string): boolean => { + return bcrypt.compareSync(data, hash); +}; diff --git a/src/lib/server/postdata/post.handler.ts b/src/lib/server/postdata/post.handler.ts new file mode 100644 index 0000000..b27c156 --- /dev/null +++ b/src/lib/server/postdata/post.handler.ts @@ -0,0 +1,445 @@ +import { getRandomUserAgent, getULID, sleep } from "$lib/utils"; +import { constants } from "$lib/utils/constants"; +import type { + ApiPostUserWithParent, + APISession, + Draw, + PostDataEntry, + ServerError, +} from "$lib/utils/data.types"; +import Fetch from "node-fetch"; +import { HttpsProxyAgent } from "https-proxy-agent"; + +export type APIRespnose = { + code: number; + success: boolean; + message: string; + data: T; + time: string; +}; + +export function buildMessageString( + i: number, + rows: PostDataEntry[], + distributorId: number, + dealerId: number, + drawId: number, + date: string, +) { + let message = ""; + let jumpSize = Math.floor(Math.random() * 490) + 10; + let total = 0; + let startReqId = new Date().getTime(); + let x = 0; + for (let j = i; j < i + jumpSize; j++) { + if (j >= rows.length) { + break; + } + const row = rows[j]; + const reqId = startReqId + x++; + const no = row.number.trim(); + const f = row.first; + const s = row.second; + const mTotal = f + s; + if (mTotal <= 0) { + continue; + } + total += mTotal; + message += `${reqId},${distributorId},${dealerId},${drawId},${date},${no},${f},${s},${mTotal};`; + } + message = message.slice(0, -1); + return { message, total, jumped: i + jumpSize }; +} + +export async function postDataToApi(payload: { + sessions: Record; + draw: Draw; + data: PostDataEntry[]; + users: ApiPostUserWithParent[]; +}) { + const responses = [] as APIRespnose<[]>[]; + const responsesIds = [] as { requestId: number; bookId: string }[]; + let failedResponses = 0; + let successResponses = 0; + + console.log(`[+] Sending ${payload.data.length} requests...`); + + const dataByUser = {} as Record; + for (const row of payload.data) { + const userId = row.userId ?? ""; + if (userId.length < 1) { + console.log(`[!] User not found for request ${row.userId}`); + return { + ok: false, + detail: "User not found to post data with", + errors: [{ message: "User not found for request" }] as ServerError, + }; + } + if (!dataByUser[userId]) { + dataByUser[userId] = []; + } + dataByUser[userId].push(row); + } + + try { + for (const userId in dataByUser) { + const session = payload.sessions[userId]; + const usr = payload.users.find((u) => u.userId === userId); + if (!usr) { + console.log(`[!] User ${userId} not found for posting to api`); + return { + ok: false, + detail: "User not found to post data with", + errors: [{ message: "User not found for request" }] as ServerError, + }; + } + const distId = usr.parentDistributor ?? 0; + const dealerId = Number(session.userId.split(":")[1]); + const drawId = Number(payload.draw.id.split(":")[1]); + const date = new Date().toISOString().split("T")[0]; + + let i = 0; + while (i < dataByUser[userId].length) { + let tries = 0; + while (tries < 3) { + let { message, total, jumped } = buildMessageString( + i, + dataByUser[userId], + distId, + dealerId, + drawId, + date, + ); + const res = await sendBatchRequest( + session, + dealerId, + payload.draw, + total, + message, + ); + const rj = (await res.json()) as APIRespnose<{ + bookDtos: { bookId: string; requestId: number }[]; + }>; + if (rj.code === 200 && res.status === 200) { + i = jumped; + responsesIds.push( + ...rj.data.bookDtos.map((b) => ({ + requestId: b.requestId as number, + bookId: b.bookId as string, + })), + ); + successResponses++; + break; + } + failedResponses++; + tries++; + } + + if (tries >= 3) { + console.log( + `[!] Failed to send data to api for user ${userId}, deleting all booked entries...`, + ); + console.log(responsesIds); + if (responsesIds.length > 0) { + const out = await deleteAllBookedEntries({ + data: responsesIds, + closeTime: payload.draw.closeTime, + dealerId, + drawId, + session, + }); + console.log(await out.text()); + } + return { + ok: false, + detail: "Failed to post data to API halfway through", + errors: [ + { message: "Failed to post data to API halfway through" }, + ] as ServerError, + }; + } + } + } + + console.log(`[+] Finished sending ${payload.data.length} requests`); + console.log(`[?] Failed responses: ${failedResponses}`); + console.log(`[?] Success responses: ${successResponses}`); + return { + ok: true, + detail: "Successfully sent data to api", + data: responses, + }; + } catch (err) { + console.log(err); + return { + ok: false, + detail: "Failed to send data to api", + }; + } +} + +async function sendBatchRequest( + session: APISession, + dealerId: number, + draw: Draw, + changedBalance: number, + body: string, +) { + return Fetch(`${constants.SCRAP_API_URL}/v1/book/add-multiple`, { + agent: new HttpsProxyAgent(`http://${session.ip}`), + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + "Content-Type": "application/json;charset=UTF-8", + Authorization: session.sessionToken, + "User-Agent": getRandomUserAgent(), + }, + body: JSON.stringify({ + changedBalance, + closeTime: draw.closeTime, + date: new Date().toISOString().split("T")[0], + dealerId, + drawId: Number(draw.id.split(":")[1]), + insertData: body, + }), + }); +} + +async function mockSendBatchRequest( + session: APISession, + dealerId: number, + draw: Draw, + changedBalance: number, + body: string, +) { + // between 5 to 20 ms + await sleep(Math.floor(Math.random() * 1000) + 50); + if (Math.random() < 0.005) { + return new Response( + JSON.stringify({ + code: 500, + success: false, + message: "Failed", + data: {}, + time: new Date().toISOString(), + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + statusText: "Failed", + }, + ); + } + return new Response( + JSON.stringify({ + code: 200, + success: true, + message: "Success", + data: { + bookDtos: body.split(";").map((e) => { + return { + bookId: getULID(), + requestId: +e.split(",")[0], + }; + }), + }, + time: new Date().toISOString(), + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + statusText: "OK", + }, + ); +} + +async function sendRequest( + requestId: number, + session: APISession, + body: PostDataEntry, + dealerId: number, + distributorId: number, + draw: Draw, +) { + return Fetch(`${constants.SCRAP_API_URL}/v1/book/add`, { + agent: new HttpsProxyAgent(`http://${session.ip}`), + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + "Content-Type": "application/json;charset=UTF-8", + Authorization: session.sessionToken, + "User-Agent": getRandomUserAgent(), + }, + body: JSON.stringify({ + retryIndex: 0, + requestId: requestId, + date: new Date().toISOString().split("T")[0], + drawId: Number(draw.id.split(":")[1]), + closeTime: draw.closeTime, + dealerId: dealerId, + distributorId: distributorId, + number: body.number, + first: body.first, + second: body.second, + changedBalance: body.first + body.second, + }), + }); +} + +async function mockSendRequest( + requestId: number, + session: APISession, + body: PostDataEntry, + dealerId: number, + distributorId: number, + draw: Draw, +) { + // between 5 to 15 ms + await sleep(Math.floor(Math.random() * 10 + 5)); + // // simulate a failed response, 20% of the time + if (Math.random() < 0.05) { + // return a failed response + return new Response( + JSON.stringify({ + code: 500, + success: false, + message: "Failed", + data: {}, + time: new Date().toISOString(), + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + statusText: "Failed", + }, + ); + } + return new Response( + JSON.stringify({ + code: 200, + success: true, + message: "Success", + data: {}, + time: new Date().toISOString(), + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + statusText: "OK", + }, + ); +} + +async function deleteAllBookedEntries({ + session, + data, + dealerId, + drawId, + closeTime, +}: { + session: APISession; + data: { bookId: string; requestId: number }[]; + dealerId: number; + drawId: number; + closeTime: string; +}) { + return Fetch(`${constants.SCRAP_API_URL}/v1/book/delete-multiple`, { + agent: new HttpsProxyAgent(`http://${session.ip}`), + method: "POST", + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + "Content-Type": "application/json;charset=UTF-8", + Authorization: session.sessionToken, + "User-Agent": getRandomUserAgent(), + }, + body: JSON.stringify({ + dealerId, + drawId, + closeTime, + bookIds: data.map((e) => e.bookId), + }), + }); +} + +// export async function postDataToApi(payload: { +// sessions: Record; +// draw: Draw; +// data: PostDataEntry[]; +// users: ApiPostUserWithParent[]; +// }) { +// const MAX_CONCURRENT_REQUESTS = 20; +// const responses = [] as APIRespnose<{}>[]; +// const baseReqId = new Date().getTime(); +// const reqIds = Array.from( +// { length: payload.data.length }, +// (_, i) => baseReqId + i, +// ); +// let failedResponses = 0; +// let successResponses = 0; +// console.log(`[+] Sending ${payload.data.length} requests...`); +// // "1723223505822,6339,6352,16,2024-08-09,123,10,10,20;1723223505823,6339,6352,16,2024-08-09,12,10,10,20", +// async function processBatch(batch: PostDataEntry[], indexes: number[]) { +// const promises = batch.map(async (row, idx) => { +// await sleep(Math.floor(Math.random() * 10 + 10)); +// const session = payload.sessions[row.userId ?? ""]; +// const usr = payload.users.find((u) => u.userId === row.userId); +// if (!usr) { +// return null; +// } +// let ok = false, +// tries = 0; +// while (!ok && tries < 3) { +// await sleep(Math.floor(Math.random() * 10 + 10)); +// const res = await mockSendRequest( +// reqIds[indexes[idx]], +// session, +// row, +// Number(session.userId.split(":")[1]), +// usr?.parentDistributor, +// payload.draw, +// ); +// let rj: APIRespnose<{}>; +// try { +// rj = (await res.json()) as APIRespnose<{}>; +// } catch (err) { +// console.log(err); +// tries++; +// continue; +// } +// if (res.status !== 200 || rj.code !== 200) { +// console.log( +// `Failed to send request ${reqIds[indexes[idx]]}, try ${tries}`, +// ); +// tries++; +// continue; +// } +// ok = true; +// successResponses++; +// return rj; +// } +// failedResponses++; +// return null; +// }); +// const results = await Promise.all(promises); +// for (const result of results) { +// if (!result) continue; +// responses.push(result); +// } +// } +// try { +// for (let i = 0; i < payload.data.length; i += MAX_CONCURRENT_REQUESTS) { +// const batch = payload.data.slice(i, i + MAX_CONCURRENT_REQUESTS); +// await processBatch( +// batch, +// batch.map((_, x) => i + x), +// ); +// } +// console.log(`[+] Finished sending ${payload.data.length} requests`); +// console.log(`[?] Failed responses: ${failedResponses}`); +// console.log(`[?] Success responses: ${successResponses}`); +// return responses; +// } catch (err) { +// console.log(err); +// return false; +// } +// } diff --git a/src/lib/server/postdata/postdata.gen.controller.ts b/src/lib/server/postdata/postdata.gen.controller.ts new file mode 100644 index 0000000..1a8fcbd --- /dev/null +++ b/src/lib/server/postdata/postdata.gen.controller.ts @@ -0,0 +1,447 @@ +import { dbApiPostData } from "$lib/server/db/apipostdata.db"; +import { dbApiUser } from "$lib/server/db/apiuser.db"; +import { getReducedFinalSheet } from "$lib/server/finalsheet.helpers"; +import { + adjustRatesIfDuplicatesFound, + pairRatesWithNumbers, + removeNumbersWithRepeatingDigits, + splitRatesIntoSmallerForRowsWithLargerRates, + spreadRatesForNumbersBetweenUsers, +} from "$lib/server/postdata/postdata.gen.helpers"; +import { getAllSessions } from "$lib/server/utils/session.service"; +import { getDefaultTotals, getULID } from "$lib/utils"; +import type { + ApiPostUser, + ApiPostUserWithParent, + PresetDataEntry, + PostDataEntry, + PostDataFilters, + PostDataHistoryFilters, + ReducedFinalSheetData, + ReducedFinalSheetRow, + ServerError, +} from "$lib/utils/data.types"; +import { dbPresetData } from "$lib/server/db/presetdata.db"; +import { getUsersBalance } from "$lib/server/external/api.scraping.helpers"; + +function filterMatching( + data: ReducedFinalSheetRow[], + min: number, + max: number, + sheetType: "first" | "second", +) { + let abNums = new Set(); + let abcNums = new Set(); + + for (const row of data) { + if (row.prize[sheetType] >= min && row.prize[sheetType] <= max) { + abNums.add(`${row.number[0]}${row.number[1]}`); + abcNums.add(`${row.number[0]}${row.number[1]}${row.number[2]}`); + } + } + return { abNums, abcNums }; +} + +export async function updateBalanceOfPostUsers(users: ApiPostUserWithParent[]) { + const sessions = await getAllSessions(); + const balances = [] as { id: string; balance: number }[]; + for (const user of users) { + const session = sessions.find((e) => e.value.userId === user.id); + const jwt = session?.value.sessionToken; + if (!jwt) { + return { + ok: false, + detail: `Session not found for user ${user.userId}`, + }; + } + const out = await getUsersBalance(+user.id.split(":")[1], jwt); + if (!out) { + return { + ok: false, + detail: `Error fetching balance for user ${user.userName}`, + }; + } + balances.push({ id: user.id, balance: out }); + } + await dbApiUser.updatePostUsersBalances(balances); + return { + ok: true, + detail: "", + data: users.map((u) => { + const bal = balances.find((b) => b.id === u.id); + if (!bal) { + console.log(`ERROR: Balance not found for user ${u.userName}`); + } + return { ...u, balance: bal?.balance ?? 0 }; + }), + }; +} + +export async function fetchDataForPosting( + date: string, + input: PostDataFilters, + users: ApiPostUser[], +) { + console.log(`The input ${JSON.stringify(input, null, 2)}`); + const { minPrize, maxPrize } = input; + const draw = input.draw!; + const fsData = { + id: getULID(), + date, + drawId: draw.id, + data: [], + totals: getDefaultTotals(), + } as ReducedFinalSheetData; + if (!draw) { + return { + ok: false, + detail: `Draw for the passed draw ID not found`, + data: [], + users: [], + errors: [ + { message: `Draw for the passed draw ID not found` }, + ] as ServerError, + }; + } + const data = await getReducedFinalSheet(fsData); + if (!data.ok) { + return { + ok: false, + detail: `Error compiling final sheet`, + data: [], + users: [], + errors: data.errors, + }; + } + + console.log("[+] Filtering the fs data to get the numbers"); + const filteredF = filterMatching(fsData.data, minPrize, maxPrize, "first"); + console.log( + `Filtered data: ${filteredF.abNums.size}; ${filteredF.abcNums.size}`, + ); + + // ------------------------------------------ + + let _abNums = new Set(), + _abcNums = new Set(); + for (const each of filteredF.abNums) _abNums.add(each); + for (const each of filteredF.abcNums) _abcNums.add(each); + let abNums = Array.from(_abNums), + abcNums = Array.from(_abcNums); + + if (draw.filterDuplicatesWhilePosting === true) { + console.log(`[+] Removing numbers that have repeating digits`); + console.log(`[=] Original : AB: ${abNums.length}, ABC: ${abcNums.length}`); + abNums = removeNumbersWithRepeatingDigits(abNums); + abcNums = removeNumbersWithRepeatingDigits(abcNums); + } + console.log(`[=] AB: ${abNums.length}, ABC: ${abcNums.length}`); + + console.log(`Fetching preset data`); + const presetData = await dbPresetData.getDataGroupedBySheetByDraw( + date, + +draw.id.split(":")[1], + ); + console.log(`${presetData.all.length} preset entries found`); + + for (let tries = 0; tries < 3; tries++) { + console.log(`[✍️] Try ${tries + 1} of generating the result`); + const out = await generatePostDataArrayFromBaseInfo( + input, + users, + abNums, + abcNums, + presetData, + ); + if (out.ok) { + return out; + } + if (out.detail.includes("Not enough balance")) { + return { + ok: false, + detail: `Users don't have enough balance to post the data, try reducing the rates`, + data: [], + users: [], + }; + } + } + + return { + ok: false, + detail: `Could not generate data, please try adjusting the filters`, + data: [], + }; +} + +export async function generatePostDataArrayFromBaseInfo( + input: PostDataFilters, + users: ApiPostUser[], + abNums: string[], + abcNums: string[], + presetData: { + all: PostDataEntry[]; + abData: PresetDataEntry[]; + abcData: PresetDataEntry[]; + }, +) { + console.log("[+] Spreading the rates for the numbers for all post user"); + const abData = splitRatesIntoSmallerForRowsWithLargerRates( + spreadRatesForNumbersBetweenUsers( + adjustRatesIfDuplicatesFound( + pairRatesWithNumbers(abNums, input.twoDigitRates), + presetData.abData, + ), + users.map((u) => u.userId), + ), + ); + const abcData = splitRatesIntoSmallerForRowsWithLargerRates( + spreadRatesForNumbersBetweenUsers( + adjustRatesIfDuplicatesFound( + pairRatesWithNumbers(abcNums, input.threeDigitRates), + presetData.abcData, + ), + users.map((u) => u.userId), + ), + ); + + // ------------------------------------------ + + console.log(`[+] Adding ${abData.length} ab entries to final list`); + console.log(`[+] Adding ${abcData.length} abc entries to final list`); + + const result = [] as PostDataEntry[]; + const alreadyPresent = new Set(); + for (const each of abData) { + alreadyPresent.add(each.number); + result.push(each); + } + for (const each of abcData) { + alreadyPresent.add(each.number); + result.push(each); + } + + // ------------------------------------------ + + const balanceCounts = {} as Record; + for (const each of result) { + const uid = each.userId ?? ""; + if (balanceCounts[uid] === undefined) { + balanceCounts[uid] = 0; + } + balanceCounts[uid] += each.first + each.second; + } + + // ------------------------------------------ + + console.log( + `[+] Appending up to ${presetData.all.length} entries that are not ab, abc`, + ); + for (const entry of presetData.all) { + if ( + alreadyPresent.has(entry.number) || + (entry.first < 5 && entry.second < 5) + ) { + continue; + } + const randomUserId = users[Math.floor(Math.random() * users.length)].userId; + if (balanceCounts[randomUserId] === undefined) { + balanceCounts[randomUserId] = 0; + } + balanceCounts[randomUserId] += entry.first + entry.second; + result.push({ ...entry, userId: randomUserId }); + } + + // ------------------------------------------ + + const usersTotalbalance = users.reduce((a, b) => a + (b.balance ?? 0), 0); + let totalAmtForPostingData = Object.values(balanceCounts).reduce( + (acc, curr) => acc + curr, + 0, + ); + if (usersTotalbalance < totalAmtForPostingData) { + return { + ok: false, + detail: `Not enough balance to book overall with ${usersTotalbalance} < ${totalAmtForPostingData}`, + data: [], + users: [], + errors: [ + { message: `Not enough balance to book overall` }, + ] as ServerError, + }; + } + + function isDistributionUnbalanced() { + let out = false; + for (const key in balanceCounts) { + if ( + balanceCounts[key] > (users.find((u) => u.userId === key)?.balance ?? 0) + ) { + out = true; + break; + } + } + return out; + } + + for (let tries = 0; tries < 5; tries++) { + console.log( + `Balance counts start : ${JSON.stringify(balanceCounts, null, 2)}`, + ); + + rebalancePostDataListByBalanceOfUsers(balanceCounts, users, result); + + console.log(`Balance counts final : ${JSON.stringify(balanceCounts)}`); + + let totalAmtForPostingDataAfterRebalance = Object.values( + balanceCounts, + ).reduce((acc, curr) => acc + curr, 0); + + console.log( + `Total amount for posting data after rebalance: ${totalAmtForPostingDataAfterRebalance}`, + `Total balance of users: ${JSON.stringify(users.map((u) => ({ un: u.userName, b: u.balance })))}`, + ); + + if (!isDistributionUnbalanced()) { + console.log(`[+] Distribution is balanced`); + break; + } + console.log(`[!] Rebalancing again`); + } + + if (isDistributionUnbalanced()) { + return { + ok: false, + detail: `Please regenerate dataset as the some users have not enough balance to book their entries`, + data: [], + users: [], + }; + } + + // ------------------------------------------ + + console.log(`[+] Shuffling ${result.length} entries for posting`); + shuffleArray(result); + + return { + ok: true, + detail: `Fetched the data successfully`, + data: result, + users, + errors: undefined, + }; +} + +function shuffleArray(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +export function rebalancePostDataListByBalanceOfUsers( + balanceCounts: Record, + users: ApiPostUser[], + result: PostDataEntry[], +) { + console.log( + `[+] Checking if the users have enough balance to book their assigned data`, + ); + + for (const user of users) { + const usersBalance = user.balance ?? 0; + const dueForUser = balanceCounts[user.userId] ?? 0; + if (usersBalance === 0) { + console.log(`\n[!] ${user.userName} has no balance\n`); + continue; + } + if (usersBalance >= dueForUser) { + console.log( + `[✅] ${user.userName} can book the data of ${usersBalance} > ${dueForUser} `, + ); + continue; + } + console.log( + `[!❎!] ${user.userName} can't book it all ${usersBalance} < ${dueForUser}`, + ); + + const difference = dueForUser - usersBalance; + let differenceLeft = Number(difference); // make a copy + const entriesToMove = result + .filter((r) => { + if (r.userId === user.userId && differenceLeft > 0) { + differenceLeft -= r.first + r.second; + return true; + } + }) + .map((r) => r.id); + console.log(`Have to move ${entriesToMove.length} entries to other users`); + + // find a user who has enough balance + + const userWithEnoughBalance = users.find((u) => { + return ( + (u.balance ?? 0) - balanceCounts[u.userId] >= difference && + u.userId !== user.userId + ); + }); + if (!userWithEnoughBalance) { + return { + ok: false, + detail: `No user found with enough balance to cover balance shortage of ${difference} for ${user.userName}`, + data: [], + }; + } + console.log( + `Dude has enough balance to take on this other user's expenses ': ${JSON.stringify(userWithEnoughBalance)}`, + ); + + for (let i = 0; i < result.length; i++) { + if (!entriesToMove.includes(result[i].id)) { + continue; + } + const entry = result[i]; + let amountMoved = 0; + if (entry.userId !== user.userId) { + continue; + } + entry.userId = userWithEnoughBalance.userId; + balanceCounts[userWithEnoughBalance.userId] += entry.first + entry.second; + balanceCounts[user.userId] -= entry.first + entry.second; + amountMoved += entry.first + entry.second; + if (amountMoved >= difference) { + // don't move more than the difference' + break; + } + } + console.log( + `[+] Moved ${entriesToMove.length} entries to ${userWithEnoughBalance.userName}`, + ); + } +} + +export async function fetchPostDataHistory(input: PostDataHistoryFilters) { + const { draw, date } = input; + console.log(`Fetching post data from HISTORY for draw: ${date} - ${draw.id}`); + const found = await dbApiPostData.getPostDataByDraw(date, draw.id); + if (!found) { + return { data: [], users: [], ok: false, detail: "Data not found" }; + } + console.log( + `Data found for the passed draw: ${date} - ${draw.id}, returning that`, + ); + const users = await dbApiUser.getAllPostUsers(); + const uniqueUserIds = [] as string[]; + for (const each of found) { + if (!each.userId || uniqueUserIds.includes(each.userId)) { + continue; + } + uniqueUserIds.push(each.userId); + } + return { + data: found, + users: users.filter((u) => uniqueUserIds.includes(u.userId)), + ok: true, + detail: "Data found", + }; +} diff --git a/src/lib/server/postdata/postdata.gen.helpers.ts b/src/lib/server/postdata/postdata.gen.helpers.ts new file mode 100644 index 0000000..fb910d1 --- /dev/null +++ b/src/lib/server/postdata/postdata.gen.helpers.ts @@ -0,0 +1,213 @@ +import { getULID } from "$lib/utils"; + +import type { + PostDataEntry, + FSPair, + PresetDataEntry, +} from "$lib/utils/data.types"; + +function splitRatesRelativelyEvenly( + totalRate: number, + parts: number, +): number[] { + if (totalRate < 5) { + return Array.from({ length: parts }, () => 0); + } + if (totalRate === 5) { + return Array.from({ length: parts }, (_, i) => (i === 0 ? 5 : 0)); + } + if (totalRate % 5 !== 0) { + throw new Error("Total rate must be a multiple of 5"); + } + const splits: number[] = []; + let remainingRate = totalRate; + // Distribute the rate using a weighted random approach + for (let i = 0; i < parts; i++) { + if (i === parts - 1) { + splits.push(remainingRate); + } else { + const minRate = 5; // Minimum rate for each part + const maxRate = Math.min( + remainingRate - (parts - i - 1) * minRate, + remainingRate * 0.6, + ); + const rate = + Math.floor((Math.random() * (maxRate - minRate + 1)) / 5) * 5 + minRate; + splits.push(rate); + remainingRate -= rate; + } + } + // Shuffle the array + for (let i = splits.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [splits[i], splits[j]] = [splits[j], splits[i]]; + } + // Occasionally allow for extreme distributions, but ensure at least one non-zero value + if (Math.random() < 0.05) { + // 5% chance + const extremeIndex = Math.floor(Math.random() * parts); + splits.fill(0); + splits[extremeIndex] = totalRate; + } + if (splits.reduce((a, b) => a + b, 0) !== totalRate) { + throw new Error("Splitting error"); + } + return splits; +} + +export function pairRatesWithNumbers(numbers: string[], rates: FSPair) { + const out = []; + for (let i = 0; i < numbers.length; i++) { + out.push({ + id: getULID(), + number: numbers[i].toString(), + first: rates.first, + second: rates.second, + createdAt: new Date().toISOString(), + }); + } + return out; +} + +export function adjustRatesIfDuplicatesFound( + original: PostDataEntry[], + newRows: PresetDataEntry[], +) { + const originalNumbers = original.map((e) => e.number); + const newNumbers = newRows.map((e) => e.number); + const duplicates = originalNumbers.filter((n) => newNumbers.includes(n)); + if (duplicates.length === 0) { + return original; + } + const out = [] as PostDataEntry[]; + for (const entry of original) { + if (!duplicates.includes(entry.number)) { + out.push(entry); + continue; + } + let first = entry.first; + let second = entry.second; + // now add rates from newRows + const newRow = newRows.find((e) => e.number === entry.number); + if (newRow) { + first += newRow.first; + second += newRow.second; + } + out.push({ ...entry, first, second }); + } + return out; +} + +export function spreadRatesForNumbersBetweenUsers( + originalData: PostDataEntry[], + userIds: string[], +) { + const dataWithSplitRates = [] as { first: number[]; second: number[] }[]; + for (const entry of originalData) { + const { first, second } = entry; + if (first % 5 !== 0 || second % 5 !== 0) { + throw new Error("Rates must be multiples of 5"); + } + const fRates = splitRatesRelativelyEvenly(first, userIds.length); + const sRates = splitRatesRelativelyEvenly(second, userIds.length); + dataWithSplitRates.push({ first: fRates, second: sRates }); + } + const out = [] as PostDataEntry[]; + for (let uIdx = 0; uIdx < userIds.length; uIdx++) { + let rIdx = 0; + for (const entry of originalData) { + let f = dataWithSplitRates[rIdx].first[uIdx] ?? 0; + let s = dataWithSplitRates[rIdx].second[uIdx] ?? 0; + if (f > 0 || s > 0) { + out.push({ + ...entry, + first: f, + second: s, + userId: userIds[Math.floor(Math.random() * userIds.length)], + }); + } + rIdx++; + } + } + return out; +} + +export function splitRatesIntoSmallerForRowsWithLargerRates( + data: PostDataEntry[], +) { + const out = [] as PostDataEntry[]; + + function getRNBtw1And6(n: number) { + const quotient = n / 5; + return Math.floor(((Math.random() * quotient) % 6) + 1); + } + + for (const entry of data) { + if (entry.first < 5 && entry.second < 5) { + continue; + } else if (entry.first === 5 && entry.second === 5) { + out.push(entry); + continue; + } + + const firstSplit = splitRatesRelativelyEvenly( + entry.first, + getRNBtw1And6(entry.first), + ); + const secondSplit = splitRatesRelativelyEvenly( + entry.second, + getRNBtw1And6(entry.second), + ); + + const maxLength = Math.max(firstSplit.length, secondSplit.length); + + const firstPadded = firstSplit.concat( + Array(maxLength - firstSplit.length).fill(0), + ); + const secondPadded = secondSplit.concat( + Array(maxLength - secondSplit.length).fill(0), + ); + + // shuffle the two + for (let i = firstPadded.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [firstPadded[i], firstPadded[j]] = [firstPadded[j], firstPadded[i]]; + const k = Math.floor(Math.random() * (i + 1)); + [secondPadded[i], secondPadded[k]] = [secondPadded[k], secondPadded[i]]; + } + + for (let i = 0; i < maxLength; i++) { + const f = firstPadded[i]; + const s = secondPadded[i]; + if (f < 5 && s < 5) { + continue; + } + out.push({ ...entry, first: f, second: s }); + } + } + + // verify that all original rates are preserved, and that the sum of the new rates is equal to the sum of the original rates + const originalSum = data.reduce( + (acc, curr) => acc + curr.first + curr.second, + 0, + ); + const newSum = out.reduce((acc, curr) => acc + curr.first + curr.second, 0); + if (originalSum !== newSum) { + console.log( + `[---] Original and new sums are not matching at all (${originalSum} !== ${newSum})`, + ); + throw new Error("Sum of rates is not equal"); + } + + return out; +} + +export function removeNumbersWithRepeatingDigits(nos: string[]): string[] { + const out = new Set(); + for (const no of nos) { + if (new Set(no).size === no.length) { + out.add(no); + } + } + return Array.from(out); +} diff --git a/src/lib/server/session.helpers.ts b/src/lib/server/session.helpers.ts new file mode 100755 index 0000000..c0db485 --- /dev/null +++ b/src/lib/server/session.helpers.ts @@ -0,0 +1,103 @@ +import { redis } from "./connectors/redis"; +import { getUUID } from "$lib/utils"; +import type { Session } from "$lib/utils/data.types"; + +export const defaultTTL = 60 * 60 * 6; + +export const generateSession = async ( + username: string, + userType: string, + ip: string, + userAgent: string, + ttl?: number, +) => { + ttl = ttl || defaultTTL; + const sId = getUUID(); + await deleteMatchingSessions(username, ip, userAgent); + await redis.setex( + sId, + ttl, + ip + "|" + userAgent + "|" + username + "|" + userType + "|" + sId, + ); + await redis.sadd("session:" + username, sId); + return sId; +}; + +export const getSession = async (sId: string) => { + const session = await redis.get(sId); + if (!session) return null; + return parseSession(session); +}; + +export const getSessions = async (username: string) => { + const sIds = await redis.smembers("session:" + username); + if (!sIds) return null; + const _sessions = await redis.mget(...sIds); + const sessions = []; + for (const session of _sessions) { + if (!session) continue; + sessions.push(parseSession(session)); + } +}; + +export const isSessionValid = async ( + sId: string, + ip: string, + userAgent: string, +) => { + const s = await getSession(sId); + if (!s) return false; + if (s.ip !== ip && s.userAgent === userAgent) return false; + if (s.userAgent !== userAgent && s.ip === ip) return false; + if (s.userAgent !== userAgent && s.ip !== ip) return false; + return true; +}; + +export const isAlreadyLoggedIn = async ( + username: string, + ip: string, + userAgent: string, +) => { + const sIds = await redis.smembers("session:" + username); + if (!sIds) return false; + for (const sId of sIds) { + if (await isSessionValid(sId, ip, userAgent)) return true; + } + return false; +}; + +export const deleteSession = async (sId: string) => { + const session = await getSession(sId); + if (!session) return false; + await redis.del(sId); + await redis.srem("session:" + session.username, sId); + return true; +}; + +export const deleteMatchingSessions = async ( + username: string, + ip: string, + userAgent: string, +) => { + const sIds = await redis.smembers("session:" + username); + if (!sIds) return false; + for (const sId of sIds) { + if (await isSessionValid(sId, ip, userAgent)) { + await deleteSession(sId); + } + } + return true; +}; + +export const deleteAllSessions = async (username: string) => { + const sIds = await redis.smembers("session:" + username); + if (!sIds) return false; + await redis.del(...sIds); + await redis.del("session:" + username); + return true; +}; + +const parseSession = (session: string) => { + const [ip, userAgent, username, userType, sId] = session.split("|"); + return { sId, ip, userAgent, username, userType } as Session; +}; diff --git a/src/lib/server/test.booking.helpers.ts b/src/lib/server/test.booking.helpers.ts new file mode 100755 index 0000000..e69de29 diff --git a/src/lib/server/utils/session.service.ts b/src/lib/server/utils/session.service.ts new file mode 100644 index 0000000..1c0f55c --- /dev/null +++ b/src/lib/server/utils/session.service.ts @@ -0,0 +1,63 @@ +import { pickRandomIP } from "$lib/utils"; +import { constants } from "$lib/utils/constants"; +import type { APISession } from "$lib/utils/data.types"; +import { redis } from "../connectors/redis"; +import { testIfSessionIsValid } from "../external/api.scraping.helpers"; + +export async function getSessionFromStore(sid: string) { + const out = await redis.get(sid); + if (out === null) { + return; + } + return JSON.parse(out) as APISession; +} + +export async function setSessionToRedis(sessionKey: string, userId: string) { + let key = constants.SCRAP_API_SESSION_KEY; + if (userId) { + key = `apisession:${userId}`; + } + console.log("Setting session to redis", key, sessionKey); + const session: APISession = { + ip: pickRandomIP(), + sessionToken: sessionKey, + userId, + }; + await redis.setex(key, 86400, JSON.stringify(session)); +} + +export async function isSessionValidInStore(userId?: string) { + let key = constants.SCRAP_API_SESSION_KEY; + if (userId) { + key = `apisession:${userId}`; + } + try { + const value = JSON.parse((await redis.get(key)) ?? "") as APISession | null; + if (value === null) { + return { valid: false }; + } + return await testIfSessionIsValid(value.sessionToken); + } catch (err) { + return false; + } +} + +export async function removeSessionFromStore(userId?: string) { + try { + let key = constants.SCRAP_API_SESSION_KEY; + if (userId) { + key = `apisession:${userId}`; + } + await redis.del(key); + } catch (err) {} +} + +export async function getAllSessions() { + const keys = await redis.keys("apisession:*"); + const sessions = []; + for (const key of keys) { + const value = JSON.parse((await redis.get(key)) ?? "{}") as APISession; + sessions.push({ key, value }); + } + return sessions; +} diff --git a/src/lib/stores/booking.state.ts b/src/lib/stores/booking.state.ts new file mode 100755 index 0000000..05958c3 --- /dev/null +++ b/src/lib/stores/booking.state.ts @@ -0,0 +1,75 @@ +import type { Draw, BookingEntry } from "$lib/utils/data.types"; +import { writable } from "svelte/store"; + +export type SyncState = { + data: BookingEntry[]; + isSyncing: boolean; + lastSynced: string; +}; + +export const syncState = writable({ + data: [], + isSyncing: false, + lastSynced: new Date().toLocaleString(), +}); + +export function setSyncState(state: SyncState) { + syncState.update((old) => { + return { ...old, ...state }; + }); + setSyncStateToLocalStorage(state); +} + +export function removeEntriesFromSyncState(ids: string[]) { + syncState.update((old) => { + return { + isSyncing: false, + lastSynced: new Date().toISOString(), + data: old.data.filter((entry) => !ids.includes(entry.id)), + }; + }); +} + +export function setSyncStateToLocalStorage(state: SyncState) { + window.localStorage.setItem("syncstate", JSON.stringify(state)); +} + +export function getSyncStateFromLocalStorage() { + const out = JSON.parse( + // @ts-ignore + window.localStorage.getItem("syncstate") || null + ) as SyncState | null; + return out; +} + +export const bookingPanelData = writable([]); + +export const selectedEntriesMap = writable({} as Record); + +export const bookingPanelState = writable({ + chosenDraw: {} as Draw, + chosenScheme: "", + isPermutationModeSelected: false, + keepRatesPostBookingSubmit: false, +}); + +type SetBookingPanelStatePayload = { + chosenDraw?: Draw; + chosenScheme?: string; + isPermutationModeSelected?: boolean; + keepRatesPostBookingSubmit?: boolean; +}; + +export const setBookingPanelState = (state: SetBookingPanelStatePayload) => { + bookingPanelState.update((older) => { + return { ...older, ...state }; + }); +}; + +export const resetSchemeStatus = () => { + bookingPanelState.update((state) => ({ + ...state, + chosenScheme: "", + isPermutationModeSelected: false, + })); +}; diff --git a/src/lib/trpc/client.ts b/src/lib/trpc/client.ts new file mode 100755 index 0000000..fc4395e --- /dev/null +++ b/src/lib/trpc/client.ts @@ -0,0 +1,12 @@ +import type { Router } from "$lib/trpc/router"; +import { createTRPCClient, type TRPCClientInit } from "trpc-sveltekit"; + +let browserClient: ReturnType>; + +export function trpc(init?: TRPCClientInit) { + const isBrowser = typeof window !== "undefined"; + if (isBrowser && browserClient) return browserClient; + const client = createTRPCClient({ init }); + if (isBrowser) browserClient = client; + return client; +} diff --git a/src/lib/trpc/context.ts b/src/lib/trpc/context.ts new file mode 100755 index 0000000..fbca428 --- /dev/null +++ b/src/lib/trpc/context.ts @@ -0,0 +1,17 @@ +import { getSession } from "$lib/server/session.helpers"; +import { constants } from "$lib/utils/constants"; +import type { RequestEvent } from "@sveltejs/kit"; +import type { inferAsyncReturnType } from "@trpc/server"; + +// we're not using the event parameter is this example, +// hence the eslint-disable rule +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function createContext(event: RequestEvent) { + return { + session: await getSession( + event.cookies.get(constants.SESSION_KEY_NAME) ?? "" + ), + }; +} + +export type Context = inferAsyncReturnType; diff --git a/src/lib/trpc/router.ts b/src/lib/trpc/router.ts new file mode 100755 index 0000000..360988d --- /dev/null +++ b/src/lib/trpc/router.ts @@ -0,0 +1,22 @@ +import { apiAuthRouter } from "./routers/apiauth.router"; +import { apiDataRouter } from "./routers/apidata.router"; +import { apiUserRouter } from "./routers/apiuser.router"; +import { bookingRouter } from "./routers/booking.router"; +import { drawRouter } from "./routers/draws.router"; +import { presetDataRouter } from "./routers/presetdata.router"; +import { postDataApiRouter } from "./routers/postdata.router"; +import { sessionRouter } from "./routers/session.router"; +import { createTRPCRouter } from "./t"; + +export const router = createTRPCRouter({ + session: sessionRouter, + apiAuth: apiAuthRouter, + apiData: apiDataRouter, + apiUser: apiUserRouter, + draw: drawRouter, + presetData: presetDataRouter, + postData: postDataApiRouter, + booking: bookingRouter, +}); + +export type Router = typeof router; diff --git a/src/lib/trpc/routers/apiauth.router.ts b/src/lib/trpc/routers/apiauth.router.ts new file mode 100755 index 0000000..3b35021 --- /dev/null +++ b/src/lib/trpc/routers/apiauth.router.ts @@ -0,0 +1,102 @@ +import { getSessionToken } from "$lib/server/external/api.scraping.helpers"; +import { TRPCError } from "@trpc/server"; +import { createTRPCRouter, protectedProcedure } from "../t"; +import { constants } from "$lib/utils/constants"; +import { getUUID } from "$lib/utils"; +import { z } from "zod"; +import { dbApiUser } from "$lib/server/db/apiuser.db"; +import type { ServerError } from "$lib/utils/data.types"; +import { + isSessionValidInStore, + removeSessionFromStore, + setSessionToRedis, +} from "$lib/server/utils/session.service"; + +export const apiAuthRouter = createTRPCRouter({ + getCaptcha: protectedProcedure.mutation(async () => { + try { + const uuid = getUUID(); + const res = await fetch( + `${constants.SCRAP_API_URL}/verify/image?uuid=${uuid}`, + { + headers: { + ...constants.SCRAP_API_BASE_HEADERS, + Accept: + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", + }, + }, + ); + const bloob = await res.blob(); + const imageBuffer = Buffer.from(await bloob.arrayBuffer()); + const base64String = imageBuffer.toString("base64"); + return { id: uuid, image: base64String }; + } catch (err) { + console.log(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error getting captcha image.", + }); + } + }), + + getNewSession: protectedProcedure + .input( + z.object({ + captchaId: z.string().min(1), + captchaAnswer: z.string().min(1), + userId: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + const { captchaId, captchaAnswer } = input; + let { userId, userType, password } = + await dbApiUser.getRandomDistributor(); + if (input.userId) { + let _user = await dbApiUser.getUserById(input.userId); + if (!_user) { + return { + success: false, + errors: [{ message: "User not found." }], + }; + } + userId = _user.userId; + userType = _user.userType; + password = _user.password; + } + const token = await getSessionToken({ + code: captchaAnswer, + verifyToken: captchaId, + userId: userId, + userType: userType, + password: password, + }); + console.log("[=] Token Response :: ", JSON.stringify(token, null, 2)); + if (!token.ok) { + return { + success: false, + errors: [{ message: token.message }], + }; + } + await setSessionToRedis(token.message, input.userId ?? ""); + return { success: true, errors: [] as ServerError }; + }), + + isApiSessionValid: protectedProcedure + .input( + z.object({ + checkingUserSession: z.boolean(), + userId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return { valid: await isSessionValidInStore(input.userId) }; + }), + + logoutUser: protectedProcedure + .input(z.object({ userId: z.string().optional() })) + .mutation(async ({ input }) => { + const { userId } = input; + await removeSessionFromStore(userId); + return { success: true, errors: [] as ServerError }; + }), +}); diff --git a/src/lib/trpc/routers/apidata.router.ts b/src/lib/trpc/routers/apidata.router.ts new file mode 100755 index 0000000..cc9a5f7 --- /dev/null +++ b/src/lib/trpc/routers/apidata.router.ts @@ -0,0 +1,187 @@ +import { redis } from "$lib/server/connectors/redis"; +import { dbApiData } from "$lib/server/db/apidata.db"; +import { dbDraw } from "$lib/server/db/apidraw.db"; +import { dbApiUser } from "$lib/server/db/apiuser.db"; +import { getData } from "$lib/server/external/api.scraping.helpers"; +import { getReducedFinalSheet } from "$lib/server/finalsheet.helpers"; +import { getDefaultTotals, getULID } from "$lib/utils"; +import { constants } from "$lib/utils/constants"; +import { + ApiUserTypes, + type APISession, + type ReducedFinalSheetData, + type ServerError, +} from "$lib/utils/data.types"; +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../t"; + +const lastFetched = { + get: async () => { + const out = await redis.get(constants.LAST_FETCHED_KEY); + if (out === null) { + return "Not fetched yet"; + } + return out; + }, + set: async () => { + await redis.set(constants.LAST_FETCHED_KEY, new Date().toISOString()); + }, +}; + +export const apiDataRouter = createTRPCRouter({ + getDealersAndDraws: protectedProcedure.query(async () => { + const draws = await dbDraw.getAllDraws(true); + const dealers = await dbApiUser.allUsersOfType(ApiUserTypes.DEALER); + const lf = await lastFetched.get(); + return { users: dealers, draws, lastFetched: lf }; + }), + + refetchData: protectedProcedure + .input( + z.object({ + userIds: z.array(z.string()), + drawId: z.string(), + targetDate: z.string(), + }), + ) + .mutation(async ({ input }) => { + const { userIds, targetDate, drawId } = input; + if (userIds.length < 1) { + return { + detail: "No users selected", + success: false, + errors: [ + { message: "No users selected to refetch data for" }, + ] as ServerError, + }; + } + const sess = JSON.parse( + (await redis.get(constants.SCRAP_API_SESSION_KEY)) ?? "", + ) as APISession; + if (sess === null) { + return { + detail: "API Session expired", + success: false, + errors: [ + { + message: + "API Session expired, get a new api session and try again", + }, + ] as ServerError, + }; + } + const userIdsInt = userIds.map((x) => parseInt(x.split(":")[1])); + const out = await getData( + sess.sessionToken, + userIdsInt, + parseInt(drawId.split(":")[1]), + targetDate, + ); + if (!out.ok) { + return { + success: false, + detail: "Error fetching data", + errors: [{ message: out.message }] as ServerError, + }; + } + const dataCount = out.data.length; + await dbApiData.upsertData(out.data, targetDate); + return { + detail: `Scraped ${dataCount} entries for ${userIds.length} users`, + success: true, + errors: [] as ServerError, + }; + }), + + getDataByFilters: protectedProcedure + .input( + z.object({ date: z.string(), drawId: z.string(), userId: z.string() }), + ) + .mutation(async ({ input }) => { + const { date, drawId, userId } = input; + const data = await dbApiData.getBookingEntriesForDealer( + date, + drawId.split(":")[1], + userId.split(":")[1], + true, + ); + return { data }; + }), + + getReducedFinalSheet: protectedProcedure + .input(z.object({ date: z.string(), drawId: z.string() })) + .mutation(async ({ input }) => { + const { date, drawId } = input; + const draw = await dbDraw.getDraw(drawId); + const fsData = { + id: getULID(), + date, + drawId, + data: [], + totals: getDefaultTotals(), + } as ReducedFinalSheetData; + if (!draw) { + return { + ok: false, + detail: `Draw for the passed draw ID not found`, + data: fsData, + errors: [ + { message: `Draw for the passed draw ID not found` }, + ] as ServerError, + }; + } + console.log("Fetching data"); + const data = await getReducedFinalSheet(fsData); + console.log(data); + if (!data.ok) { + return { + ok: false, + detail: `Error compiling final sheet`, + data: fsData, + errors: data.errors, + }; + } + return { + ok: true, + detail: `Final sheet for ${date}, draw ${draw.title} has been compiled`, + data: fsData, + errors: [] as ServerError, + }; + }), + + getFinalSheetRow: protectedProcedure + .input( + z.object({ date: z.string(), drawId: z.string(), number: z.string() }), + ) + .mutation(async ({ input }) => { + return { + ok: true, + data: {}, + errors: [] as ServerError, + }; + }), + + delDataOlderThan2Weeks: protectedProcedure.mutation(async () => { + await dbApiData.deleteDataOlderThan2Weeks(); + return { ok: true, detail: "Data older than 2 weeks has been deleted" }; + }), + + postTestBooking: protectedProcedure + .input(z.object({ drawId: z.string(), date: z.string() })) + .mutation(async () => { + return { + ok: true, + detail: "API not live", + errors: [] as ServerError, + }; + // console.log("GENERATING TEST DATA :: ", drawId, date); + // const testData = await getTestBookingData(drawId, date); + // // console.log(testData); + // await dbApiData.upsertData(testData, date); + // return { + // ok: true, + // detail: "Test booking committed", + // errors: [] as ServerError, + // }; + }), +}); diff --git a/src/lib/trpc/routers/apiuser.router.ts b/src/lib/trpc/routers/apiuser.router.ts new file mode 100644 index 0000000..a6622ab --- /dev/null +++ b/src/lib/trpc/routers/apiuser.router.ts @@ -0,0 +1,37 @@ +import { createTRPCRouter, protectedProcedure } from "../t"; +import { ApiUserTypes, zApiPostUser } from "$lib/utils/data.types"; +import { dbApiUser } from "$lib/server/db/apiuser.db"; +import { z } from "zod"; + +export const apiUserRouter = createTRPCRouter({ + getAllDistributors: protectedProcedure.query(async () => { + return await dbApiUser.allUsersOfType(ApiUserTypes.DISTRIBUTOR); + }), + getAllDealers: protectedProcedure.query(async () => { + return await dbApiUser.allUsersOfType(ApiUserTypes.DEALER); + }), + getAllDistributorsCount: protectedProcedure.query(async () => { + return await dbApiUser.getUserTypeCount(ApiUserTypes.DISTRIBUTOR); + }), + getAllDealersCount: protectedProcedure.query(async () => { + return await dbApiUser.getUserTypeCount(ApiUserTypes.DEALER); + }), + getDistributorsWithTheirChildren: protectedProcedure.query(async () => { + const users = await dbApiUser.getAllDistributorsWithTheirChildren(); + return { users }; + }), + + getAllDealersPostUserFormat: protectedProcedure.query(async () => { + return await dbApiUser.allUsersOfTypeLimitedInfo(ApiUserTypes.DEALER); + }), + + getAllPostUsers: protectedProcedure.query(async () => { + return await dbApiUser.getAllPostUsers(); + }), + + setPostDataFlagForUser: protectedProcedure + .input(z.object({ users: z.array(zApiPostUser) })) + .mutation(async ({ input }) => { + await dbApiUser.setPostDataFlagForUsers(input.users); + }), +}); diff --git a/src/lib/trpc/routers/booking.router.ts b/src/lib/trpc/routers/booking.router.ts new file mode 100755 index 0000000..f4844e7 --- /dev/null +++ b/src/lib/trpc/routers/booking.router.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../t"; +import { dbDraw } from "$lib/server/db/apidraw.db"; +import { DEFAULT_TZ } from "$lib/utils/constants"; +import { + zBookingEntry, + type BookingEntry, + type ServerError, +} from "$lib/utils/data.types"; +import { surreal } from "$lib/server/connectors/surreal.db"; +import { parseToDateString } from "$lib/utils/datetime.helper.utils"; + +function getTodaysTableName() { + const today = parseToDateString(new Date()); + return `booking${today.replaceAll("-", "")}`; +} + +export const bookingRouter = createTRPCRouter({ + getPanelData: protectedProcedure.query(async () => { + const draws = await dbDraw.getAllDraws(true); + const timeInDrawsTz = new Date().toLocaleString("en-US", { + timeZone: DEFAULT_TZ, + }); + return { draws, timeInDrawsTz: timeInDrawsTz }; + }), + + getBookingData: protectedProcedure + .input(z.object({ drawId: z.string() })) + .mutation(async ({ input }) => { + const { drawId } = input; + const date = parseToDateString(new Date()); + const tn = getTodaysTableName(); + const did = parseInt(drawId.split(":")[1]); + const [out] = await surreal.query<[BookingEntry[]]>( + `select * from type::table($table) where drawId = $drawId and bookDate = $bookDate order by requestId desc`, + { table: tn, drawId: did, bookDate: date } + ); + return { data: out.result ?? [], errors: [] as ServerError }; + }), + + syncBooking: protectedProcedure + .input(z.object({ data: z.array(zBookingEntry) })) + .mutation(async ({ input }) => { + const tableName = getTodaysTableName(); + const syncedEntriesIds = [] as string[]; + if (input.data.length > 0) { + await surreal.insert( + tableName, + input.data.map((e) => { + syncedEntriesIds.push(`${e.id}`); + return { + ...e, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + }) + ); + } + return { detail: "Add Booking api donezo", syncedEntriesIds }; + }), + + deleteBooking: protectedProcedure + .input(z.object({ bookingIds: z.array(z.string()) })) + .mutation(async ({ input }) => { + await Promise.all( + input.bookingIds.map(async (id) => { + await surreal.delete(id); + }) + ); + return { detail: `Deleted ${input.bookingIds.length} Entries` }; + }), +}); diff --git a/src/lib/trpc/routers/draws.router.ts b/src/lib/trpc/routers/draws.router.ts new file mode 100755 index 0000000..f52d8e4 --- /dev/null +++ b/src/lib/trpc/routers/draws.router.ts @@ -0,0 +1,30 @@ +import { createTRPCRouter, protectedProcedure } from "../t"; +import { dbDraw } from "$lib/server/db/apidraw.db"; +import { z } from "zod"; +import { zDraw } from "$lib/utils/data.types"; + +export const drawRouter = createTRPCRouter({ + getAllDraws: protectedProcedure.query(async () => { + return await dbDraw.getAllDraws(true); + }), + + getCurrentTime: protectedProcedure.query(async () => { + const now = new Date(); + const timezone = "Asia/Karachi"; + const nowKarachi = new Date( + now.toLocaleString("en-US", { timeZone: timezone }), + ); + console.log(nowKarachi.toLocaleString()); + return { now: nowKarachi }; + }), + + savePresetInfoForDraws: protectedProcedure + .input(z.object({ draws: z.array(zDraw) })) + .mutation(async ({ input }) => { + console.log("savePresetInfoForDraws", input); + for (const draw of input.draws) { + await dbDraw.updateDrawPresetInfo(draw); + } + return { success: true }; + }), +}); diff --git a/src/lib/trpc/routers/postdata.router.ts b/src/lib/trpc/routers/postdata.router.ts new file mode 100644 index 0000000..eedc9fd --- /dev/null +++ b/src/lib/trpc/routers/postdata.router.ts @@ -0,0 +1,212 @@ +import { dbApiPostData } from "$lib/server/db/apipostdata.db"; +import { dbApiUser } from "$lib/server/db/apiuser.db"; +import { postDataToApi } from "$lib/server/postdata/post.handler"; +import { getAllSessions } from "$lib/server/utils/session.service"; +import { getULID } from "$lib/utils"; +import { + type APISession, + type ServerError, + zPostDataEntry, + zPostDataFilters, + zPostDataHistoryFilters, +} from "$lib/utils/data.types"; +import { createTRPCRouter, protectedProcedure } from "../t"; +import { z } from "zod"; +import { + fetchDataForPosting, + fetchPostDataHistory, + updateBalanceOfPostUsers, +} from "$lib/server/postdata/postdata.gen.controller"; +import { redis } from "$lib/server/connectors/redis"; +import { constants } from "$lib/utils/constants"; + +async function hasPostSession() { + const out = await redis.get(constants.POST_SESSION_KEY); + if (out === null) { + await redis.set(constants.POST_SESSION_KEY, "1"); + return false; + } + return out === "1"; +} + +async function removePostSession() { + await redis.del(constants.POST_SESSION_KEY); +} + +export const postDataApiRouter = createTRPCRouter({ + fetchPostDataHistory: protectedProcedure + .input(zPostDataHistoryFilters) + .mutation(async ({ input }) => { + return await fetchPostDataHistory(input); + }), + + hasPosted: protectedProcedure + .input(zPostDataHistoryFilters) + .query(async ({ input }) => { + return { + hasPosted: await dbApiPostData.doesPostHistoryDataExist( + input.date, + input.draw?.id ?? "", + ), + }; + }), + + getPostDataForPreview: protectedProcedure + .input(zPostDataFilters) + .query(async ({ input }) => { + const date = input.date; + if (!input.draw) { + return { + ok: false, + detail: "Draw is required", + data: [], + errors: undefined, + }; + } + + console.log("[+] Fetching the users with updated balances"); + const balOut = await updateBalanceOfPostUsers( + await dbApiUser.getAllPostUsersWithParentUsers(), + ); + if (!balOut.ok || !balOut.data) { + return { ok: false, detail: balOut.detail, data: [], users: [] }; + } + const users = balOut.data; + console.log(`[=] ${users.length} users found`); + console.log(users); + + const result = await fetchDataForPosting(date, input, users); + console.log("result.data.length = ", result.data.length); + return result; + }), + + post: protectedProcedure + .input( + z.object({ + yes: zPostDataFilters, + data: z.array(zPostDataEntry), + }), + ) + .mutation(async ({ input }) => { + if (await hasPostSession()) { + return { + ok: false, + detail: + "Already posting data, please wait for the current session to finish", + errors: [ + { + message: + "Already posting data, please wait for the current session to finish", + }, + ] as ServerError, + }; + } + const date = input.yes.date; + const draw = input.yes.draw; + if (!draw) { + await removePostSession(); + return { + ok: false, + detail: "Draw is required", + errors: [{ message: "Draw is required" }] as ServerError, + }; + } + const drawId = draw.id; + + console.log("[+] Fetching the users"); + const users = await dbApiUser.getAllPostUsersWithParentUsers(); + console.log(users); + const balOut = await updateBalanceOfPostUsers(users); + if (!balOut.ok || !balOut.data) { + await removePostSession(); + return { ok: false, detail: balOut.detail, data: [], users: [] }; + } + console.log(`[=] ${users} users found`); + console.log(users); + + console.log("[+] Preparing the sessions for posting"); + const sessions = await getAllSessions(); + const userSessions = {} as Record; + for (const each of sessions) { + const targetUser = users.find( + (u) => each.key.includes(u.id) || each.value.userId === u.userId, + ); + if (!targetUser) continue; + userSessions[targetUser?.userId ?? ""] = each.value; + } + + if (Object.keys(userSessions).length !== users.length) { + await removePostSession(); + return { + ok: false, + detail: `Some users don't have a session to post data with`, + errors: [ + { message: "Some users don't have a session to post data with" }, + ], + }; + } + + console.log(userSessions, sessions); + + let data = input.data; + if (input.data.length < 1) { + console.log("No data found from preview, generating a list"); + const _out = await fetchDataForPosting(date, input.yes, balOut.data); + if (!_out.ok) { + await removePostSession(); + return _out; + } + data = _out.data; + } + + if (data.length < 1) { + await removePostSession(); + return { + ok: false, + detail: "No data found to post for the selected date and draw", + errors: [ + { message: "No data found to post for the selected date and draw" }, + ], + }; + } + + console.log(`[+] Posting ${input.data.length} entries to the API`); + + console.time("Time taken to post data to the API"); + const res = await postDataToApi({ + sessions: userSessions, + data, + users: users, + draw: draw, + }); + console.timeEnd("Time taken to post data to the API"); + + if (!res.ok) { + await removePostSession(); + return { ok: false, detail: res.detail }; + } + + console.log(`[+] Data posted to the API successfully`); + + await dbApiPostData.upsertData({ + id: getULID(), + drawId: +drawId.split(":")[1], + bookDate: date, + data, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + // Update the balance of the users after posting to the API + await updateBalanceOfPostUsers(users); + + console.log("[+] Data saved to the database"); + + await removePostSession(); + return { + ok: true, + detail: "Data successfully posted to API", + errors: undefined, + }; + }), +}); diff --git a/src/lib/trpc/routers/presetdata.router.ts b/src/lib/trpc/routers/presetdata.router.ts new file mode 100644 index 0000000..fedc871 --- /dev/null +++ b/src/lib/trpc/routers/presetdata.router.ts @@ -0,0 +1,35 @@ +import { dbPresetData } from "$lib/server/db/presetdata.db"; +import { zDDFilters, zPresetDataEntry } from "$lib/utils/data.types"; +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../t"; + +export const presetDataRouter = createTRPCRouter({ + getAll: protectedProcedure.input(zDDFilters).mutation(async ({ input }) => { + const { draw, date } = input; + if (!draw) { + return { ok: false, detail: "Draw is required to fetch data", data: [] }; + } + return { + data: await dbPresetData.getDataByDraw(date, +draw.id.split(":")[1]), + ok: true, + detail: "Data found", + }; + }), + + insert: protectedProcedure + .input(z.array(zPresetDataEntry)) + .mutation(async ({ input }) => { + return { + ok: true, + detail: "Data inserted", + data: await dbPresetData.insertData(input), + }; + }), + + delete: protectedProcedure + .input(z.object({ date: z.string(), ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + await dbPresetData.deleteDataByIds(input.date, input.ids); + return { ok: true, detail: "Data deleted" }; + }), +}); diff --git a/src/lib/trpc/routers/session.router.ts b/src/lib/trpc/routers/session.router.ts new file mode 100755 index 0000000..7825148 --- /dev/null +++ b/src/lib/trpc/routers/session.router.ts @@ -0,0 +1,13 @@ +import type { SessionData } from "$lib/utils/data.types"; +import { createTRPCRouter, protectedProcedure } from "../t"; + +export const sessionRouter = createTRPCRouter({ + getSession: protectedProcedure.query(async ({ ctx }) => { + return { + user: { + username: ctx.session.username, + userType: ctx.session.userType, + } as SessionData + } + }), +}); diff --git a/src/lib/trpc/t.ts b/src/lib/trpc/t.ts new file mode 100755 index 0000000..05bed68 --- /dev/null +++ b/src/lib/trpc/t.ts @@ -0,0 +1,29 @@ +import type { Context } from "$lib/trpc/context"; +import { TRPCError, initTRPC } from "@trpc/server"; +import { ZodError } from "zod"; + +export const t = initTRPC.context().create({ + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +export const createTRPCRouter = t.router; + +export const publicProcedure = t.procedure; + +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.username) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ ctx: { session: { ...ctx.session } } }); +}); + +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/src/lib/trpc/trpc.ts b/src/lib/trpc/trpc.ts new file mode 100755 index 0000000..a256880 --- /dev/null +++ b/src/lib/trpc/trpc.ts @@ -0,0 +1,19 @@ +// src/lib/trpc.ts +import type { Router } from "./router"; +import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; + +import type { QueryClient } from "@tanstack/svelte-query"; +import { svelteQueryWrapper } from "trpc-svelte-query-adapter"; + +const client = createTRPCProxyClient({ + links: [httpBatchLink({ url: "/trpc" })], +}); + +export function trpc(queryClient?: QueryClient) { + return svelteQueryWrapper({ + client, + queryClient, + }); +} + +export type TRPC = ReturnType; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100755 index 0000000..2623cf6 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,188 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { cubicOut } from "svelte/easing"; +import type { TransitionConfig } from "svelte/transition"; +import type { Draw, ServerError } from "./utils/data.types"; +import { rng } from "./utils/rng"; +import { ulid } from "ulid"; +import { v4 } from "uuid"; +import { parseISO } from "date-fns"; +import { PROXIES } from "./utils/constants"; + +export function hasDrawBeenClosed( + date: string, + draw: Draw | undefined, + now: Date = new Date(), +) { + let hasTimePassed = false; + + const chosenDate = new Date(parseISO(date)); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + let _ct = draw?.closeTime?.split(" ")[1] ?? ""; + const closeTime = new Date(parseISO(`${date}T${_ct}`)); + + if (today > chosenDate || now > closeTime) { + hasTimePassed = true; + } else { + hasTimePassed = false; + } + + console.log("==============="); + console.log(now); + console.log(closeTime); + console.log(`Has time passed : ${hasTimePassed}`); + + return hasTimePassed; +} + +export const parseToErrorList = (errors: ServerError | Array) => { + if (Array.isArray(errors)) { + return errors; + } + return [errors]; +}; + +export const parseToSelectList = ( + arr: string[], +): { label: string; value: string; id: number }[] => { + return arr.map((item, index) => { + return { label: item, value: item, id: index }; + }); +}; + +export const randomString = (length: number) => { + const chars = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let result = ""; + for (let i = length; i > 0; --i) result += chars[rng(0, chars.length - 1)]; + return result; +}; + +export function pickRandomIP() { + return PROXIES[rng(0, PROXIES.length - 1)]; +} + +export const getUUID = () => { + return v4(); +}; + +export const getULID = () => { + return ulid(); +}; + +function getRandomVersion() { + const majorVersion = rng(1, 10); + const minorVersion = rng(0, 10); + const patchVersion = rng(0, 10); + return `${majorVersion}.${minorVersion}.${patchVersion}`; +} + +export const getRandomUserAgent = () => { + const browsers = ["Chrome", "Firefox", "Safari", "Opera", "Edge"]; + const operatingSystems = [ + "Windows NT 10.0", + "Windows NT 6.3", + "Macintosh; Intel Mac OS X 10_15_6", + "Macintosh; Intel Mac OS X 10_14_6", + "X11; Ubuntu; Linux x86_64", + "X11; Fedora; Linux x86_64", + "X11; Linux x86_64", + "Android 10", + "Android 9", + "Android 8.1", + "Android 8.0", + "iPhone; CPU iPhone OS 14_0 like Mac OS X", + "iPhone; CPU iPhone OS 13_7 like Mac OS X", + "iPhone; CPU iPhone OS 12_4_8 like Mac OS X", + ]; + const randomBrowser = browsers[Math.floor(Math.random() * browsers.length)]; + const randomOS = + operatingSystems[Math.floor(Math.random() * operatingSystems.length)]; + const userAgent = `${randomBrowser}/${getRandomVersion()} (${randomOS})`; + return userAgent; +}; + +export const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export const getParsedObject = (data: any) => { + const yeye: Record = {}; + for (const each of data.data.data) { + const un = each.user.userName; + if (!Object.keys(yeye).includes(un)) { + yeye[each.user.userName] = []; + } + yeye[each.user.userName].push(each.book); + } + return yeye; +}; + +export const getDefaultTotals = () => { + return { + commission: { first: 0, second: 0 }, + netRate: { first: 0, second: 0 }, + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + frequency: { first: 0, second: 0 }, + }; +}; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +type FlyAndScaleParams = { + y?: number; + x?: number; + start?: number; + duration?: number; +}; + +export const flyAndScale = ( + node: Element, + params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }, +): TransitionConfig => { + const style = getComputedStyle(node); + const transform = style.transform === "none" ? "" : style.transform; + + const scaleConversion = ( + valueA: number, + scaleA: [number, number], + scaleB: [number, number], + ) => { + const [minA, maxA] = scaleA; + const [minB, maxB] = scaleB; + + const percentage = (valueA - minA) / (maxA - minA); + const valueB = percentage * (maxB - minB) + minB; + + return valueB; + }; + + const styleToString = ( + style: Record, + ): string => { + return Object.keys(style).reduce((str, key) => { + if (style[key] === undefined) return str; + return str + `${key}:${style[key]};`; + }, ""); + }; + + return { + duration: params.duration ?? 200, + delay: 0, + css: (t) => { + const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); + const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); + const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); + + return styleToString({ + transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, + opacity: t, + }); + }, + easing: cubicOut, + }; +}; diff --git a/src/lib/utils/booking/booking-field-controller.ts b/src/lib/utils/booking/booking-field-controller.ts new file mode 100755 index 0000000..e69de29 diff --git a/src/lib/utils/booking/booking.sync.ts b/src/lib/utils/booking/booking.sync.ts new file mode 100755 index 0000000..c126fc5 --- /dev/null +++ b/src/lib/utils/booking/booking.sync.ts @@ -0,0 +1,105 @@ +import type { + BookingEntry, + BookingInputValues, + ServerError, +} from "../data.types"; +import { getAllMatchingChildNumbersObject } from "../finalsheet.utils"; +import { permutations } from "../permutations"; +import { getULID } from ".."; +import { parseToDateString } from "../datetime.helper.utils"; + +export function getParsedBookingEntries( + values: BookingInputValues, + chosenLexiCodes: string[], + isPossibleCombinationMode: boolean, + drawId: number +) { + const MAX_NO_LEN = 4; + const inputNumbers = values.number.split("."); + const out = [] as BookingEntry[]; + const today = new Date(); + // INFO: these 2 IDs would be modified during the algorithm run anyways, so this is okay + const commonInfo = { + distributorId: 0, + dealerId: 0, + drawId, + changedBalance: 0, + bookDate: parseToDateString(today), + }; + if (chosenLexiCodes.length > 0) { + for (const number of inputNumbers) { + let __numbers = new Set(); + __numbers.add(number); + if (isPossibleCombinationMode) { + __numbers = findAllNumberPossibleCombinations(number, MAX_NO_LEN); + } + for (const each of __numbers) { + const children = getAllMatchingChildNumbersObject(each); + for (const lc of chosenLexiCodes) { + const no = children[lc as keyof typeof children]; + const id = getULID(); + out.push({ + id, + number: no, + first: parseInt(values.first[lc]), + second: parseInt(values.second[lc]), + requestId: id, + ...commonInfo, + }); + } + } + } + } else { + for (const number of inputNumbers) { + const id = getULID(); + out.push({ + id, + number, + first: parseInt(values.default.first), + second: parseInt(values.default.second), + requestId: id, + ...commonInfo, + }); + } + } + return out; +} + +function findAllNumberPossibleCombinations(nombor: string, maxLength: number) { + const out = new Set(); + for (const each_perm of permutations(nombor.split(""), maxLength)) { + out.add(each_perm.join("")); + } + return out; +} + +export function ensureInputIsValid( + values: BookingInputValues, + chosenLexiCodes: string[] +) { + const errors = [] as ServerError; + const rates = [ + ...Object.values(values.default), + ...Object.values(values.first), + ...Object.values(values.second), + ]; + for (const each of rates) { + if (each.length < 1) { + continue; + } + if (parseInt(each) % 5 !== 0) { + errors.push({ message: `${each} rate must be a multiple of 5` }); + } + } + for (const lexiCode of chosenLexiCodes) { + const lexiCodeMinLen = lexiCode.replaceAll("+", "").length; + for (const number of values.number.split(".")) { + if (number.length < lexiCodeMinLen) { + errors.push({ + message: `${number} must be at least ${lexiCodeMinLen} digits to book ${lexiCode}`, + }); + } + } + } + return errors; +} diff --git a/src/lib/utils/booking/data.entry.helpers.ts b/src/lib/utils/booking/data.entry.helpers.ts new file mode 100755 index 0000000..a8e4fb0 --- /dev/null +++ b/src/lib/utils/booking/data.entry.helpers.ts @@ -0,0 +1,35 @@ +export const parseNumberInput = ( + input: string, + maxlen: number, + isPermutationMode: boolean +): string => { + let _s = input.replaceAll("x", "+").replace(/[^0-9\.\+]/, ""); + if (isPermutationMode) { + _s = _s.replaceAll("+", ""); + } + const splits = _s.split("."); + let out = ""; + for (let i = 0; i < splits.length; i++) { + const part = splits[i]; + out += part.substring(0, maxlen) + "."; + } + return out.replace(/\.{2,}/g, ".").substring(0, out.length - 1); +}; + +export const parseRate = (rate: string): string => { + if (rate === "") { + return ""; + } + const max = 10_000; + const r = parseFloat(rate.replace(/[^015]/, "")); + if (isNaN(r)) { + return ""; + } + if (r > max) { + return max.toString(); + } + if (r % 5 !== 0 && r !== 1) { + return (r - (r % 5)).toString(); + } + return r.toString(); +}; diff --git a/src/lib/utils/constants.ts b/src/lib/utils/constants.ts new file mode 100755 index 0000000..489c250 --- /dev/null +++ b/src/lib/utils/constants.ts @@ -0,0 +1,213 @@ +export const constants = { + SESSION_KEY_NAME: "SID", + SESSION_EXPIRE_TIME_MS: 6 * 60 * 60 * 1000, + POST_SESSION_KEY: "postsession", + LAST_FETCHED_KEY: "LAST_FETCHED", + SCRAP_API_URL: "https://ritmuglobal.com:8443/lottery", + SCRAP_API_SESSION_KEY: "SRAJWT", + SCRAP_API_BASE_HEADERS: { + Host: "ritmuglobal.com:8443", + "Sec-Ch-Ua": '"Not/A)Brand";v="8", "Chromium";v="126"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"Windows"', + "Sec-Fetch-Site": "cross-site", + "Sec-Fetch-Mode": "no-cors", + "Sec-Fetch-Dest": "image", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-US,en;q=0.9", + "Access-Control-Allow-Origin": "*", + Accept: "application/json, text/plain, */*", + Origin: "https://www.ritmuglobal.com", + Referer: "https://www.ritmuglobal.com/", + Priority: "u=1, i", + }, +}; + +const baseIp = "45.137.23.193"; +const proxyUsername = "f8n2yeqrjhcy"; +const proxyPassword = "2f309nqecdyjhgykubvcnhe"; + +export const PROXIES = [ + `${proxyUsername}:${proxyPassword}@${baseIp}:11420`, + `${proxyUsername}:${proxyPassword}@${baseIp}:11421`, + `${proxyUsername}:${proxyPassword}@${baseIp}:11422`, +]; + +export const LS_SYNCSTATE_KEY = "syncstate"; + +export const COLOR_TRANSITION = "transition-colors duration-100 ease-in-out"; + +export const DEFAULT_TZ = "Asia/Karachi"; + +// 20% commission +export const COMMISSION_PERCENTAGE = 0.2; + +export const NUMBERS_IN_FIRST_DRAW = 1; +export const NUMBERS_IN_SECOND_DRAW = 3; + +export const LEXICODES = [ + "a", + "+a", + "++a", + "+++a", + "ab", + "+ab", + "a+b", + "+a+b", + "++ab", + "a++b", + "abc", + "+abc", + "a+bc", + "ab+c", + "abcd", +]; + +export const LEXICODES_SORTED_FOR_INPUT = [ + "+++a", + "++a", + "+a", + "a", + "a++b", + "+a+b", + "++ab", + "a+b", + "+ab", + "ab", + "ab+c", + "a+bc", + "+abc", + "abc", + "abcd", +].reverse(); + +export const MAX_RATES = { + "1": 10_000, + "2": 5000, + "3": 2000, + "4": 1000, +}; + +export const LEXICODE_PRIZE_PERCENTAGES = { + first: { + a: 80, + "+a": 80, + "++a": 80, + "+++a": 80, + ab: 80, + "+ab": 80, + "a+b": 80, + "++ab": 80, + "+a+b": 80, + "a++b": 80, + abc: 80, + "+abc": 80, + "a+bc": 80, + "ab+c": 80, + abcd: 60, + }, + second: { + a: 26.66, + "+a": 26.66, + "++a": 26.66, + "+++a": 26.66, + ab: 26.66, + "+ab": 26.66, + "a+b": 26.66, + "++ab": 26.66, + "+a+b": 26.66, + "a++b": 26.66, + abc: 26.66, + "+abc": 26.66, + "a+bc": 26.66, + "ab+c": 26.66, + abcd: 20, + }, +} as const; + +export const LEXICODE_MATHCER_PATTERNS = { + a: /^\d{1}((\+|x){0}|(\+|x){1,3}?)$/, + "+a": /^(\+|x)\d{1}((\+|x){0}|(\+|x){1,2}?)$/, + "++a": /^(\+|x){2}\d{1}(\+|x)?$/, + "+++a": /^(\+|x){3}\d{1}$/, + ab: /^\d{2}((\+|x){0}|(\+|x){1,2}?)$/, + "+ab": /^(\+|x)\d{2}((\+|x){0}|(\+|x){1}?)?$/, + "a+b": /^\d{1}(\+|x){1}\d{1}((\+|x){0}|(\+|x){1}?)?$/, + "++ab": /^(\+|x){2}\d{2}$/, + "+a+b": /^(\+|x){1}\d{1}(\+|x){1}\d{1}$/, + "a++b": /^\d{1}(\+|x){2}\d{1}$/, + abc: /^\d{3}(\+|x)?$/, + "+abc": /^(\+|x){1}\d{3}$/, + "a+bc": /^\d{1}(\+|x){1}\d{2}$/, + "ab+c": /^\d{2}(\+|x){1}\d{1}$/, + abcd: /^\d{4}$/, +} as const; + +export const SCHEMES = { + normal: { + "1 digit": [ + "a.+a", + "a.++a", + "a.+++a", + "+a.++a", + "+a.+++a", + "++a.+++a", + "a.+a.++a.+++a", + ], + "2 digit": ["ab.+ab", "ab.++ab", "+ab.++ab", "ab.+ab.++ab"], + "3 digit": [ + "abc.ab", + "abc.++ab", + "abc.+abc", + "abc.+ab.++ab", + "abc.+abc.ab.++ab", + ], + "4 digit": [ + "abcd.ab", + "abcd.+ab", + "abcd.++ab", + "abcd.ab.+ab", + "abcd.ab.++ab", + "abcd.+ab.++ab", + "abcd.abc", + "abcd.+abc", + "abcd.abc.ab", + "abcd.abc.+ab", + "abcd.+abc.ab", + "abcd.+abc.+ab", + "abcd.abc.+abc.ab.+ab.++ab", + ], + }, + permutations: { + default: [ + "ab", + "+ab", + "++ab", + "a+b", + "+a+b", + "a++b", + "abc", + "+abc", + "a+bc", + "ab+c", + "abcd", + ], + "2 digit": ["ab.+ab", "ab.++ab", "+ab.++ab", "ab.+ab.++ab"], + "3 digit": ["abc.ab", "abc.++ab", "abc.+abc", "abc.+abc.ab.++ab"], + "4 digit": [ + "abcd.ab", + "abcd.+ab", + "abcd.++ab", + "abcd.ab.+ab", + "abcd.+ab.++ab", + "abcd.ab.+ab.++ab", + "abcd.abc", + "abcd.+abc", + "abcd.abc.ab", + "abcd.abc.+ab", + "abcd.+abc.ab", + "abcd.+abc.+ab", + "abcd.abc.+abc.ab.+ab.++ab", + ], + }, +}; diff --git a/src/lib/utils/data.types.ts b/src/lib/utils/data.types.ts new file mode 100755 index 0000000..ba6886f --- /dev/null +++ b/src/lib/utils/data.types.ts @@ -0,0 +1,382 @@ +import { z } from "zod"; + +export type Session = { + sId: string; + ip: string; + userAgent: string; + username: string; + userType: string; +}; + +export type APISession = { + ip: string; + sessionToken: string; + userId: string; +}; + +export const zAuthPayload = z.object({ + username: z.string().min(4).max(64), + password: z.string().min(8).max(64), +}); + +export const zUser = z.object({ + id: z.string().length(16), + createdAt: z.string(), + updatedAt: z.string(), + username: z.string().min(4).max(64), + password: z.string().min(8).max(64), + userType: z.string().min(4).max(5), + association: z.string(), +}); + +export const zLooseUser = z.object({ + id: z.string().length(16).optional(), + createdAt: z.string().optional(), + updatedAt: z.string().optional(), + username: z.string().min(4).max(64), + password: z.string().min(8).max(64), + userType: z.string().min(4).max(5), + association: z.string(), +}); + +export const zApiUser = z.object({ + id: z.string().length(16), + userType: z.number(), + disableBooking: z.string().nullable().optional(), + sendVoucher: z.string().nullable().optional(), + voucherGenerated: z.string().nullable().optional(), + parentAdmin: z.number(), + parentDistributor: z.number(), + userName: z.string(), + userCity: z.string().nullable().optional(), + userId: z.string(), + password: z.string(), + accessDenied: z.number(), + phoneNumber: z.string(), + emailAddress: z.string(), + disable: z.number(), + commission: z.number(), + commissionPangora: z.number(), + allowTitles: z.string(), + specialDealer: z.number(), + allowBalance: z.number(), + balance: z.number(), + profitlossShare: z.number(), + shareProfitonly: z.number(), + allowRemoveold: z.number(), + removeDays: z.number().nullable().optional(), + language: z.number(), + postData: z.boolean().nullable().optional(), + createdAt: z.string().nullable(), + updatedAt: z.string().nullable(), +}); + +export const zApiPostUser = z.object({ + id: z.string(), + userName: z.string(), + userId: z.string(), + postData: z.boolean(), + balance: z.number().optional(), +}); + +export const zLooseApiUser = z.object({ + id: z.string().length(16).optional(), + userType: z.number().optional(), + disableBooking: z.string().nullable().optional(), + sendVoucher: z.string().nullable().optional(), + voucherGenerated: z.string().nullable().optional(), + parentAdmin: z.number(), + parentDistributor: z.number(), + userName: z.string(), + userCity: z.string().nullable().optional(), + userId: z.string().optional(), + password: z.string(), + accessDenied: z.number(), + phoneNumber: z.string(), + emailAddress: z.string(), + disable: z.number(), + commission: z.number(), + commissionPangora: z.number(), + allowTitles: z.string(), + specialDealer: z.number(), + allowBalance: z.number(), + balance: z.number(), + profitlossShare: z.number(), + shareProfitonly: z.number(), + allowRemoveold: z.number(), + removeDays: z.number().nullable().optional(), + language: z.number().optional(), + postData: z.boolean().nullable().optional(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export const zDraw = z.object({ + id: z.string(), + title: z.string(), + closeTime: z.string(), + filterDuplicatesWhilePosting: z.boolean(), + drawType: z.number(), + adminId: z.number(), + abRateF: z.coerce.number(), + abRateS: z.coerce.number(), + abcRateF: z.coerce.number(), + abcRateS: z.coerce.number(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type Draw = z.infer; + +export const zBookingEntry = z.object({ + id: z.string(), + distributorId: z.number(), + dealerId: z.number(), + drawId: z.number(), + bookDate: z.string(), + number: z.string(), + first: z.number(), + second: z.number(), + changedBalance: z.number(), + sheetName: z.string().nullable().optional(), + sheetId: z.string().nullable().optional(), + requestId: z.string(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type BookingEntry = z.infer; + +export const zPostDataEntry = z.object({ + id: z.string(), + requestId: z.string().nullable().optional(), + number: z.string(), + first: z.number(), + second: z.number(), + userId: z.string().nullable().optional(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type PostDataEntry = z.infer; + +const zPostDataHistory = z.object({ + id: z.string(), + data: z.array(zPostDataEntry), + drawId: z.number(), + bookDate: z.string(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type PostDataHistory = z.infer; + +export const zPresetDataEntry = z.object({ + id: z.string(), + drawId: z.number(), + bookDate: z.string(), + number: z.string(), + first: z.number(), + second: z.number(), + createdAt: z.string().nullable().optional(), + dealerId: z.number().optional().nullable(), +}); + +export type PresetDataEntry = z.infer; + +export const fsPair = z.object({ + first: z.number(), + second: z.number(), +}); + +export const zLexiCodeCacheObject = z.object({ + number: z.string(), + rate: fsPair, + prize: fsPair, + frequency: fsPair, +}); + +export const reducedFinalSheetRow = z.object({ + id: z.string(), + number: z.string(), + frequency: fsPair, + frequencies: z.object({ + // a: fsPair, + // ab: fsPair, + // abc: fsPair, + // "+abc": fsPair, + // "a+bc": fsPair, + // "ab+c": fsPair, + abcd: fsPair, + }), + rate: fsPair, + prize: fsPair, + profit: fsPair, +}); + +export const zfinalSheetRow = z.object({ + id: z.string(), + number: z.string(), + frequency: fsPair, + rate: fsPair, + prize: fsPair, + profit: fsPair, + + a: zLexiCodeCacheObject, + xa: zLexiCodeCacheObject, + xxa: zLexiCodeCacheObject, + xxxa: zLexiCodeCacheObject, + ab: zLexiCodeCacheObject, + xab: zLexiCodeCacheObject, + axb: zLexiCodeCacheObject, + xaxb: zLexiCodeCacheObject, + xxab: zLexiCodeCacheObject, + axxb: zLexiCodeCacheObject, + abc: zLexiCodeCacheObject, + xabc: zLexiCodeCacheObject, + axbc: zLexiCodeCacheObject, + abxc: zLexiCodeCacheObject, + abcd: zLexiCodeCacheObject, +}); + +export type ServerError = Array<{ + message: string; + value?: string; + meta?: any; +}>; + +export const UserTypes = { ADMIN: "ADMIN", USER: "USER" }; + +export const ApiUserTypes = { ADMIN: 1, DISTRIBUTOR: 2, DEALER: 3 }; + +export type User = z.infer; + +export type LooseUser = z.infer; + +export type ApiUser = z.infer; + +export type ApiPostUser = z.infer; + +export type ApiPostUserWithParent = ApiPostUser & { + parentAdmin: number; + parentDistributor: number; +}; + +export type LooseApiUser = z.infer; + +export type LexiCodeCacheObject = z.infer; + +export type SimpleLexiCodeObject = { number: string; lexiCode: string }; + +export type FinalSheetRow = z.infer; + +export type ReducedFinalSheetRow = z.infer; + +export type FSPair = z.infer; + +export type FSTotals = { + rate: FSPair; + prize: FSPair; + commission: FSPair; + netRate: FSPair; + frequency: FSPair; +}; + +export type ReducedFinalSheetData = { + id: string; + date: string; + drawId: string; + data: Array; + totals: FSTotals; + createdAt?: string; + updatedAt?: string; +}; + +export type FinalSheetData = { + id: string; + date: string; + drawId: string; + data: Array; + totals: FSTotals; + createdAt?: string; + updatedAt?: string; +}; + +export type AuthPayload = z.infer; + +export type SessionData = { + username: string; + userType: string; +}; + +export type BookingInputValues = { + number: string; + default: { first: string; second: string }; + first: Record; + second: Record; +}; + +export const zPostDataHistoryFilters = z.object({ + date: z.string(), + draw: zDraw, +}); + +export type PostDataHistoryFilters = z.infer; + +export const zDDFilters = z.object({ + date: z.string(), + draw: zDraw.optional(), +}); + +export type DDFilters = z.infer; + +export const zDDUserFilters = z.object({ + date: z.string(), + draw: zDraw.optional(), + user: zApiUser.optional(), +}); + +export type DDUserFilters = z.infer; + +export const zPostDataFilters = z.object({ + date: z.string(), + draw: zDraw.optional().nullable(), + minPrize: z.coerce.number(), + maxPrize: z.coerce.number(), + twoDigitRates: fsPair, + threeDigitRates: fsPair, + customData: z.string(), +}); + +export type PostDataFilters = z.infer; + +export const DEFAULT_RANDOM_DISTRIBUTOR = { + id: "apiuser:6339", + userType: 2, + disableBooking: null, + sendVoucher: null, + voucherGenerated: null, + parentAdmin: 15, + parentDistributor: 0, + userName: "Baba Sagar", + userCity: "Shikar pur", + userId: "317XY3", + password: "405613", + accessDenied: 0, + phoneNumber: "", + emailAddress: "", + disable: 0, + commission: 20.0, + commissionPangora: 20.0, + allowTitles: ",7,8,9,10,11,12,13,14,15,16,30,31,32,", + specialDealer: 0, + allowBalance: 1, + balance: 30094.905, + profitlossShare: 50.0, + shareProfitonly: 0, + allowRemoveold: 0, + removeDays: 30, + language: 0, + createdAt: new Date().toString(), + updatedAt: new Date().toString(), +} as unknown as ApiUser; diff --git a/src/lib/utils/datetime.helper.utils.ts b/src/lib/utils/datetime.helper.utils.ts new file mode 100755 index 0000000..ca3db60 --- /dev/null +++ b/src/lib/utils/datetime.helper.utils.ts @@ -0,0 +1,59 @@ +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; +import duration from "dayjs/plugin/duration"; +import { DEFAULT_TZ } from "./constants"; + +dayjs.extend(timezone); +dayjs.extend(utc); +dayjs.extend(duration); + +export function calculateTimeRemaining( + closingTime: string, + returnClosed: boolean, + timezone: string = DEFAULT_TZ +): string { + const currentDate = dayjs().tz(timezone).format("YYYY-MM-DD"); + const adjustedClosingTime = `${currentDate} ${closingTime.split(" ")[1]}`; + const closingDateTime = dayjs.tz( + adjustedClosingTime, + "YYYY-MM-DD HH:mm:ss", + timezone + ); + const currentDateTime = dayjs().tz(timezone); + + const isValid = closingDateTime.isValid(); + const isBefore = closingDateTime.isBefore(currentDateTime); + const isSame = closingDateTime.isSame(currentDateTime); + + if (!isValid || isBefore || isSame) { + return returnClosed ? "closed" : "00:00:00"; + } + + const diffDuration = dayjs.duration(closingDateTime.diff(currentDateTime)); + const formattedTime = dayjs + .utc(diffDuration.asMilliseconds()) + .format("HH:mm:ss"); + + return formattedTime; +} + +export function isDrawClosed( + closingTime: string, + timezone: string = DEFAULT_TZ +): boolean { + const out = calculateTimeRemaining(closingTime, true, timezone); + return out === "closed"; +} + +export function parseToDateString(date: Date): string { + return dayjs(date).format("YYYY-MM-DD"); +} + +// export const getCurrentDateInTimeZone(timeZone: string): string { +// const now = dayjs().utc(); +// const localDateTime = now.tz(timeZone); +// const formattedDate = localDateTime.format('YYYY-MM-DD'); +// +// return formattedDate; +// } diff --git a/src/lib/utils/entry.parser.ts b/src/lib/utils/entry.parser.ts new file mode 100644 index 0000000..eef47f9 --- /dev/null +++ b/src/lib/utils/entry.parser.ts @@ -0,0 +1,42 @@ +import { getULID } from "$lib/utils"; +import type { PostDataEntry } from "./data.types"; + +export function parseEntriesFromMessage(message: string): PostDataEntry[] { + const out = [] as PostDataEntry[]; + const lines = message.split("\n"); + for (let line of lines) { + line = line.trim().toLowerCase(); + if (line.length < 1 || (!line.includes("f") && !line.includes("s"))) { + continue; + } + let [numsStr, ratesStr] = line.split("f"); + if (!ratesStr || ratesStr === "") { + [numsStr, ratesStr] = line.split("s"); + if (!ratesStr || ratesStr === "") { + continue; + } + } + let fRate = 0; + let sRate = 0; + if (ratesStr.includes("s")) { + const [fPart, sPart] = ratesStr.split("s"); + fRate = Number(fPart); + sRate = Number(sPart); + } else { + fRate = Number(ratesStr); + } + const nums = numsStr.split("."); + for (const num of nums) { + if (num.length < 1 || num.match(/[^0-9]/i) || num.length > 4) { + continue; + } + out.push({ + id: getULID(), + number: num, + first: fRate, + second: sRate, + }); + } + } + return out; +} diff --git a/src/lib/utils/finalsheet.utils.ts b/src/lib/utils/finalsheet.utils.ts new file mode 100755 index 0000000..19fb2d6 --- /dev/null +++ b/src/lib/utils/finalsheet.utils.ts @@ -0,0 +1,237 @@ +import { + COMMISSION_PERCENTAGE, + LEXICODE_MATHCER_PATTERNS, + LEXICODE_PRIZE_PERCENTAGES, +} from "./constants"; +import type { LexiCodeCacheObject, SimpleLexiCodeObject } from "./data.types"; + +export const round2D = (num: number) => { + return Number(num.toFixed(2)); +}; + +export const getNoOfDigits = (lexiCode: string) => { + const lens = { 1: 10, 2: 100, 3: 1000, 4: 10000 }; + return lens[lexiCode.split("+").join("").length as keyof typeof lens]; +}; + +export const getNetRate = (rate: number) => { + return round2D(rate - rate * COMMISSION_PERCENTAGE); +}; + +export const getCommisionAmt = (rate: number) => { + return round2D(rate * COMMISSION_PERCENTAGE); +}; + +export const getAllMatchingChildNumbers = (parent: string) => { + const out = [] as SimpleLexiCodeObject[]; + out.push({ number: `${parent[0]}`, lexiCode: "a" }); + out.push({ + number: getMatchingChild(parent, "+a"), + lexiCode: "+a", + }); + out.push({ + number: getMatchingChild(parent, "++a"), + lexiCode: "++a", + }); + out.push({ + number: getMatchingChild(parent, "+++a"), + + lexiCode: "+++a", + }); + out.push({ + number: `${parent[0]}${parent[1]}`, + lexiCode: "ab", + }); + out.push({ + number: getMatchingChild(parent, "+ab"), + lexiCode: "+ab", + }); + out.push({ + number: getMatchingChild(parent, "a+b"), + + lexiCode: "a+b", + }); + out.push({ + number: getMatchingChild(parent, "+a+b"), + lexiCode: "+a+b", + }); + out.push({ + number: getMatchingChild(parent, "++ab"), + lexiCode: "++ab", + }); + out.push({ + number: getMatchingChild(parent, "a++b"), + lexiCode: "a++b", + }); + out.push({ + number: `${parent[0]}${parent[1]}${parent[2]}`, + lexiCode: "abc", + }); + out.push({ + number: getMatchingChild(parent, "+abc"), + lexiCode: "+abc", + }); + out.push({ + number: getMatchingChild(parent, "a+bc"), + lexiCode: "a+bc", + }); + out.push({ + number: getMatchingChild(parent, "ab+c"), + lexiCode: "ab+c", + }); + out.push({ number: parent, lexiCode: "abcd" }); + return out; +}; + +export const getAllMatchingChildNumbersObject = (parent: string) => { + return { + a: `${parent[0]}`, + "+a": getMatchingChild(parent, "+a"), + "++a": getMatchingChild(parent, "++a"), + "+++a": getMatchingChild(parent, "+++a"), + ab: `${parent[0]}${parent[1]}`, + "+ab": getMatchingChild(parent, "+ab"), + "a+b": getMatchingChild(parent, "a+b"), + "+a+b": getMatchingChild(parent, "+a+b"), + "++ab": getMatchingChild(parent, "++ab"), + "a++b": getMatchingChild(parent, "a++b"), + abc: `${parent[0]}${parent[1]}${parent[2]}`, + "+abc": getMatchingChild(parent, "+abc"), + "a+bc": getMatchingChild(parent, "a+bc"), + "ab+c": getMatchingChild(parent, "ab+c"), + abcd: parent, + }; +}; + +function getMatchingChild(n: string, lexiCode: string): string { + const lopn = n.length; + if (lexiCode === "a") { + return `${n[0]}`; + } else if (lexiCode === "+a") { + if (lopn === 1) { + return `+${n[n.length - 1]}`; + } else { + return `+${n[1]}`; + } + } else if (lexiCode === "++a") { + if (lopn === 1 || lopn === 2) { + return `++${n[n.length - 1]}`; + } else { + return `++${n[2]}`; + } + } else if (lexiCode === "+++a") { + if (lopn === 1 || lopn === 2 || lopn === 3) { + return `+++${n[n.length - 1]}`; + } else { + return `+++${n[3]}`; + } + } else if (lexiCode === "ab") { + return `${n[0]}${n[1]}`; + } else if (lexiCode === "+ab") { + if (lopn === 2) { + return `+${n[0]}${n[1]}`; + } else { + return `+${n[1]}${n[2]}`; + } + } else if (lexiCode === "a+b") { + if (lopn === 2) { + return `${n[0]}+${n[1]}`; + } else { + return `${n[0]}+${n[2]}`; + } + } else if (lexiCode === "++ab") { + if (lopn === 2 || lopn === 3) { + return `++${n[n.length - 2]}${n[n.length - 1]}`; + } else { + return `++${n[2]}${n[3]}`; + } + } else if (lexiCode === "+a+b") { + if (lopn === 2) { + return `+${n[0]}+${n[1]}`; + } else if (lopn === 3) { + return `+${n[1]}+${n[2]}`; + } else { + return `+${n[1]}+${n[3]}`; + } + } else if (lexiCode === "a++b") { + if (lopn < 4) { + return `${n[0]}++${n[n.length - 1]}`; + } else { + return `${n[0]}++${n[3]}`; + } + } else if (lexiCode === "abc") { + return `${n[0]}${n[1]}${n[2]}`; + } else if (lexiCode === "+abc") { + if (lopn === 3) { + return `+${n[0]}${n[1]}${n[2]}`; + } else { + return `+${n[1]}${n[2]}${n[3]}`; + } + } else if (lexiCode === "a+bc") { + if (lopn === 3) { + return `${n[0]}+${n[1]}${n[2]}`; + } else { + return `${n[0]}+${n[2]}${n[3]}`; + } + } else if (lexiCode === "ab+c") { + if (lopn === 3) { + return `${n[0]}${n[1]}+${n[2]}`; + } else { + return `${n[0]}${n[1]}+${n[3]}`; + } + } else if (lexiCode === "abcd") { + return n; + } + throw new Error("Invalid lexi code passed"); +} + +export function* get4DigitGenerator(): Generator { + for (let i = 0; i < 10; i++) { + for (let j = 0; j < 10; j++) { + for (let k = 0; k < 10; k++) { + for (let l = 0; l < 10; l++) { + yield `${i}${j}${k}${l}`; + } + } + } + } +} + +export const calculatePrize = ( + amount: number, + lexiCode: string, + type: "first" | "second", + noOfDigits: number +) => { + // @ts-ignore + const lexiCodePercentage = LEXICODE_PRIZE_PERCENTAGES[type][lexiCode]; + if (amount && lexiCodePercentage > 0) { + return parseInt( + (amount * noOfDigits * (lexiCodePercentage / 100)).toFixed(2) + ); + } + return 0; +}; + +export const calculateProfit = (netRate: number, prize: number) => { + return netRate - prize; +}; + +export const getLexiCode = (no: string) => { + for (const [lexicode, pattern] of Object.entries(LEXICODE_MATHCER_PATTERNS)) { + if (no.match(pattern)?.length) { + return lexicode; + } + } + console.log("No lexi code found for ", no); + return false; +}; + +export const getLexiCodeCacheObject = (no: string) => { + return { + number: no, + rate: { first: 0, second: 0 }, + prize: { first: 0, second: 0 }, + frequency: { first: 0, second: 0 }, + } as LexiCodeCacheObject; +}; diff --git a/src/lib/utils/permutations.ts b/src/lib/utils/permutations.ts new file mode 100755 index 0000000..cb35333 --- /dev/null +++ b/src/lib/utils/permutations.ts @@ -0,0 +1,52 @@ +export function permutations(array: string[], r: number) { + let n = array.length; + if (r === undefined) { + r = n; + } + if (r > n) { + return []; + } + let indices = []; + for (let i = 0; i < n; i++) { + indices.push(i); + } + let cycles = []; + for (let i = n; i > n - r; i--) { + cycles.push(i); + } + let results = []; + let res = []; + for (let k = 0; k < r; k++) { + res.push(array[indices[k]]); + } + results.push(res); + let broken = false; + while (n > 0) { + for (let i = r - 1; i >= 0; i--) { + cycles[i]--; + if (cycles[i] === 0) { + indices = indices + .slice(0, i) + .concat(indices.slice(i + 1).concat(indices.slice(i, i + 1))); + cycles[i] = n - i; + broken = false; + } else { + let j = cycles[i]; + let x = indices[i]; + indices[i] = indices[n - j]; + indices[n - j] = x; + let res = []; + for (let k = 0; k < r; k++) { + res.push(array[indices[k]]); + } + results.push(res); + broken = true; + break; + } + } + if (broken === false) { + break; + } + } + return results; +} diff --git a/src/lib/utils/rng.ts b/src/lib/utils/rng.ts new file mode 100755 index 0000000..9652884 --- /dev/null +++ b/src/lib/utils/rng.ts @@ -0,0 +1,3 @@ +export const rng = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100755 index 0000000..57db9df --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,22 @@ + + +
+
+
{$page.status}
+
+
+ {$page.error?.message ?? "An error occured"} +
+
+ + +

Back to homepage

+
+
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100755 index 0000000..caf57b1 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,5 @@ +import type { LayoutServerLoad } from "./$types"; + +export const load = (async ({ locals, url }) => { + return { user: locals.user }; +}) satisfies LayoutServerLoad; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100755 index 0000000..33599cd --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,25 @@ + + + +
+ + + + + +
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100755 index 0000000..794a885 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,14 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import { UserTypes } from "$lib/utils/data.types"; + +export const load = (async ({ url, locals }) => { + const user = locals.user; + const isAdmin = user?.userType === UserTypes.ADMIN; + const isUser = user?.userType === UserTypes.USER; + if (isAdmin) { + throw redirect(302, "/admin"); + } else if (isUser) { + throw redirect(302, "/user"); + } +}) satisfies PageServerLoad; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100755 index 0000000..ebcde45 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,6 @@ + + +
+

Home Page

+
diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte new file mode 100755 index 0000000..26b10b7 --- /dev/null +++ b/src/routes/admin/+layout.svelte @@ -0,0 +1,8 @@ + + + + diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100755 index 0000000..e6768c8 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,130 @@ + + +
+
+
+ + <Button + text={"Toggle filters"} + intent={"primaryInverted"} + disabled={isLoading} + onClick={() => (showFilters = !showFilters)} + /> + </div> + <Line /> + {#if isLoading} + <CenteredSpinner /> + {:else if $filtersQ.data} + <DataFetchingFilters + bind:showFilters + draws={$filtersQ.data.draws} + fetchDataFn={fetchFinalSheet} + fetchingFinalSheet={$finalSheetM.isLoading} + /> + {/if} + {#if $finalSheetM.isLoading} + <CenteredSpinner /> + {:else if rowCount > 0 && $finalSheetM?.data && $finalSheetM.data?.data} + <FSInfo {totals} /> + <FsTable + {api} + {rowCount} + data={$finalSheetM.data.data.data ?? []} + /> + {:else} + <span class="w-full grid place-items-center py-24"> + <Title size={"h4"} text={"No data found for chosen filters"} /> + </span> + {/if} + </div> +</section> diff --git a/src/routes/admin/admin-navbar.svelte b/src/routes/admin/admin-navbar.svelte new file mode 100755 index 0000000..5655828 --- /dev/null +++ b/src/routes/admin/admin-navbar.svelte @@ -0,0 +1,79 @@ +<script lang="ts"> + import IconSignOut from "~icons/ph/sign-out"; + import { page } from "$app/stores"; + + import type { SessionData } from "$lib/utils/data.types"; + import LinkButton from "$lib/components/atoms/link-button.svelte"; + import clsx from "clsx"; + import { goto } from "$app/navigation"; + + export let user: SessionData | undefined; + + const prefix = "/admin"; + const links = [ + { + label: "Final Sheet", + href: prefix, + }, + { + label: "API Controls", + href: prefix + "/api-controls", + }, + { + label: "Post Config", + href: prefix + "/post-data-config", + }, + { + label: "Preset Data", + href: prefix + "/post-data-panel", + }, + { + label: "Post History", + href: prefix + "/post-data-history", + }, + // { + // label: "User Data", + // href: prefix + "/user-data", + // }, + ]; + + const logOut = async () => { + await fetch("/api/auth/logout", { method: "POST" }); + goto("/auth/signin"); + }; +</script> + +<nav class="flex w-full flex-col max-w-screen shadow-sm mb-8"> + <section + class="flex w-full justify-between items-center p-4 md:p-6 lg:px-8 pb-4 md:pb-4 lg:pb-4" + > + <div class="flex items-center gap-2"> + <img + src="/favicon.png" + class="w-8 h-8 text-sky-500 md:w-10 md:h-10 lg:w-12 lg:h-12" + alt="Icon" + /> + <span class="capitalize text-md md:text-lg lg:text-xl font-medium" + >{user ? user.username : "User"}</span + > + </div> + <LinkButton iconleft={IconSignOut} text={"Logout"} onClick={logOut} /> + </section> + + <section class="max-w-screen overflow-x-auto flex md:px-8"> + {#each links as link} + <a href={link.href}> + <div + class={clsx( + "p-2 font-medium px-4 rounded-t-md w-32 text-center border-b hover:border-sky-400 hover:text-sky-500 hover:bg-sky-100 cursor-pointer duration-150 transition-colors text-sm lg:text-md", + $page.url.pathname.endsWith(link.href) + ? "border-sky-500 bg-sky-50 text-sky-700" + : "border-transparent", + )} + > + {link.label} + </div> + </a> + {/each} + </section> +</nav> diff --git a/src/routes/admin/api-controls/+page.server.ts b/src/routes/admin/api-controls/+page.server.ts new file mode 100755 index 0000000..810ae3f --- /dev/null +++ b/src/routes/admin/api-controls/+page.server.ts @@ -0,0 +1,55 @@ +import { fail } from "@sveltejs/kit"; +import type { Actions } from "../../$types"; +import { ApiUserTypes, type ServerError } from "$lib/utils/data.types"; +import { + getDealers, + getDistributors, +} from "$lib/server/external/api.scraping.helpers"; +import { dbApiUser } from "$lib/server/db/apiuser.db"; +import { constants } from "$lib/utils/constants"; +import fs from "fs"; +import { getSessionFromStore } from "$lib/server/utils/session.service"; + +export const actions = { + refetchDistributors: async () => { + const sess = await getSessionFromStore(constants.SCRAP_API_SESSION_KEY); + if (!sess) { + return fail(400, { + success: false, + errors: [{ message: "No api session found" }], + }); + } + const done = await getDistributors(sess.sessionToken); + console.log(`[+] ${done.data.length} distributors found`); + fs.writeFileSync("distributors.json", JSON.stringify(done.data, null, 2)); + if (!done.ok) { + return fail(400, { + success: false, + errors: [{ message: done.message }], + }); + } + await dbApiUser.upsertMany(done.data, true, ApiUserTypes.DISTRIBUTOR); + return { success: true, errors: [] as ServerError }; + }, + + refetchDealers: async () => { + const sess = await getSessionFromStore(constants.SCRAP_API_SESSION_KEY); + if (!sess) { + return fail(400, { + success: false, + errors: [{ message: "No api session found" }], + }); + } + const distributor_ids = await dbApiUser.getAllIdsByUserType( + ApiUserTypes.DISTRIBUTOR, + ); + const done = await getDealers(sess.sessionToken, distributor_ids); + console.log(`[+] ${done.dealers.length} dealers found`); + fs.writeFileSync("dealers.json", JSON.stringify(done.dealers, null, 2)); + if (done.errors.length > 0) { + return fail(400, { success: false, errors: done.errors }); + } + await dbApiUser.upsertMany(done.dealers, true, ApiUserTypes.DEALER); + return { success: true, errors: [] as ServerError }; + }, +} satisfies Actions; diff --git a/src/routes/admin/api-controls/+page.svelte b/src/routes/admin/api-controls/+page.svelte new file mode 100755 index 0000000..cf1f45f --- /dev/null +++ b/src/routes/admin/api-controls/+page.svelte @@ -0,0 +1,47 @@ +<script lang="ts"> + import Line from "$lib/components/atoms/line.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import { trpc } from "$lib/trpc/trpc"; + import FetchLatestData from "./fetch-latest-data.svelte"; + import RefetchDealers from "./refetch-dealers.svelte"; + import RefetchDistributors from "./refetch-distributors.svelte"; + import SessionVerifier from "./session-verifier.svelte"; + import { Button } from "$lib/components/ui/button"; + import toast from "svelte-french-toast"; + + const api = trpc(); + + let delDataM = api.apiData.delDataOlderThan2Weeks.createMutation({ + onSuccess: () => { + toast("Data deleted"); + }, + onError: (err) => { + toast.error(err.message); + }, + }); + + function delData() { + toast("Deleting data..."); + $delDataM.mutateAsync(); + } +</script> + +<section class="w-full grid place-items-center p-8"> + <div + class="w-full flex flex-col gap-4 max-w-3xl p-6 lg:p-8 rounded-lg shadow-sm border" + > + <Title text={"API controls"} size={"h1"} /> + <Line /> + <SessionVerifier {api} /> + <RefetchDistributors {api} /> + <RefetchDealers {api} /> + <FetchLatestData {api} /> + + <Line cls="my-2" /> + <div> + <Button variant="destructive" on:click={delData}> + Delete all data older than 2 weeks + </Button> + </div> + </div> +</section> diff --git a/src/routes/admin/api-controls/fetch-latest-data.svelte b/src/routes/admin/api-controls/fetch-latest-data.svelte new file mode 100755 index 0000000..b99812a --- /dev/null +++ b/src/routes/admin/api-controls/fetch-latest-data.svelte @@ -0,0 +1,153 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Switch from "$lib/components/atoms/switch.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import type { trpc } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import clsx from "clsx"; + import Input from "$lib/components/atoms/input.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + + export let api: ReturnType<typeof trpc>; + + let chosenDate = new Date().toISOString().split("T")[0]; + let chosenDraw = ""; + let chosenDealers = new Set(); + let userListQuery = ""; + + const addOrDelDealer = (item: any) => { + if (chosenDealers.has(item.id)) { + chosenDealers.delete(item.id); + } else { + chosenDealers.add(item.id); + } + chosenDealers = chosenDealers; + }; + + let filtersQ = api.apiData.getDealersAndDraws.createQuery(undefined, { + refetchInterval: 1000 * 60 * 30, + refetchOnWindowFocus: false, + retry: 3, + onSuccess: (d) => { + if (d.draws.length > 0) { + chosenDraw = d.draws[0].id; + } + return d; + }, + }); + $: isLoading = $filtersQ.isLoading || $filtersQ.isFetching; + + let refetchDataM = api.apiData.refetchData.createMutation({ + onSuccess: (o) => { + if (!o.success) { + for (const each of o.errors) { + toast.error(each.message); + } + } else { + toast.success(o.detail, { duration: 1000 * 10 }); + } + }, + onError: (e) => { + toast.error(e.message); + }, + }); + $: refetching = $refetchDataM.isLoading; + + const refetchDataaaaaaaaa = async () => { + toast( + "Refetching data. This may take a 1 or 2 mins, so please wait and do not refresh the page", + { icon: "ⓘ", duration: 1000 * 10 }, + ); + await $refetchDataM.mutateAsync({ + userIds: Array.from(chosenDealers) as string[], + drawId: chosenDraw, + targetDate: chosenDate, + }); + }; +</script> + +<Title text={"Fetch Data"} size={"h2"} /> + +{#if isLoading} + <div class="w-max"> + <Pill theme={"slate"} text={"Querying info..."} /> + </div> +{:else if $filtersQ.data} + <Title size={"h4"} text={"Date & Draw"} /> + <section class="flex flex-col md:flex-row gap-4 w-full"> + <Input + bind:value={chosenDate} + label={"Date"} + inputType={"date"} + placeholder={"Date"} + /> + <Select + fullWidth + label={"Draw"} + options={$filtersQ.data.draws.map((e) => { + return { label: e.title, value: e.id, id: e.id }; + })} + onSelect={(e) => { + // @ts-ignore + chosenDraw = e.target.value; + }} + /> + </section> + <Title size={"h4"} text={"Dealers"} /> + <section class="flex flex-col gap-4"> + <Input + bind:value={userListQuery} + placeholder={"Search dealers . . ."} + /> + <div + class="p-2 rounded-lg w-full border shadow-sm h-80 overflow-y-auto flex flex-col gap-1" + > + {#each $filtersQ.data.users as user} + <!-- svelte-ignore a11y-no-static-element-interactions --> + {#if user && (userListQuery === "" || user.userName + .toLowerCase() + .includes(userListQuery.toLowerCase()) || user.userId + .toLowerCase() + .includes(userListQuery.toLowerCase()))} + <div + class={clsx( + "rounded-md hover:bg-sky-200 text-sky-800 font-medium p-2 cursor-pointer transition-colors duration-100 border capitalize", + chosenDealers.has(user.id) + ? "bg-sky-300 border-sky-600" + : "bg-sky-50 border-sky-200", + )} + on:click={() => addOrDelDealer(user)} + on:touchend={() => addOrDelDealer(user)} + on:keypress={() => {}} + > + {user?.userName} - {user?.userId} + </div> + {/if} + {/each} + </div> + {#if $filtersQ.data && $filtersQ.data.users.length > 0} + <Switch + label={"Select all dealers"} + onChange={(v) => { + if (v && $filtersQ.data) { + for (const each of $filtersQ.data.users) { + if (!each) continue; + chosenDealers.add(each.id); + } + } else { + chosenDealers.clear(); + } + chosenDealers = chosenDealers; + }} + /> + {/if} + </section> + + <Button + text={refetching ? "Fetching data..." : "Fetch data"} + intent={refetching ? "ghost" : "primary"} + disabled={refetching} + onClick={refetchDataaaaaaaaa} + /> +{/if} diff --git a/src/routes/admin/api-controls/refetch-dealers.svelte b/src/routes/admin/api-controls/refetch-dealers.svelte new file mode 100755 index 0000000..fe40044 --- /dev/null +++ b/src/routes/admin/api-controls/refetch-dealers.svelte @@ -0,0 +1,65 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import type { trpc } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import type { SubmitFunction } from "./$types"; + import { enhance } from "$app/forms"; + + export let api: ReturnType<typeof trpc>; + + let dQ = api.apiUser.getAllDealersCount.createQuery(undefined, { + refetchOnWindowFocus: false, + refetchInterval: false, + retry: 3, + }); + $: isLoading = $dQ.isLoading || $dQ.isFetching; + + let refetching = false; + const formInterceptor: SubmitFunction = async () => { + refetching = true; + toast("Fetching dealers, it may take around 1 minute so please wait...", { + icon: "ⓘ", + duration: 1000 * 5, + }); + return async ({ update, result }) => { + refetching = false; + if (result.type === "failure") { + if (result.data && result.data.errors.length > 0) { + for (const each of result.data.errors) { + toast.error(each.message); + } + } + } + const isSuccess = result.type === "success"; + await update({ reset: isSuccess }); + if (isSuccess) { + toast.success("Successfuly refetched dealers"); + $dQ.refetch(); + } + }; + }; +</script> + +<Title text={"Dealers"} size={"h2"} /> + +<div class="flex w-full justify-between items-center"> + {#if isLoading} + <Pill text={"Querying..."} theme={"slate"} /> + {:else if $dQ.data} + <Pill text={`${$dQ.data.enabled} enabled`} theme={"green"} /> + {/if} + <form + method="post" + action={"/admin/api-controls/?/refetchDealers"} + use:enhance={formInterceptor} + > + <Button + disabled={refetching} + text={refetching ? "Refetching..." : "Refetch"} + intent={refetching ? "ghost" : "primary"} + otherOptions={{ type: "submit" }} + /> + </form> +</div> diff --git a/src/routes/admin/api-controls/refetch-distributors.svelte b/src/routes/admin/api-controls/refetch-distributors.svelte new file mode 100755 index 0000000..cea8562 --- /dev/null +++ b/src/routes/admin/api-controls/refetch-distributors.svelte @@ -0,0 +1,61 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import type { trpc } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import type { SubmitFunction } from "./$types"; + import { enhance } from "$app/forms"; + + export let api: ReturnType<typeof trpc>; + + let dQ = api.apiUser.getAllDistributorsCount.createQuery(undefined, { + refetchOnWindowFocus: false, + refetchInterval: false, + retry: 3, + }); + $: isLoading = $dQ.isLoading || $dQ.isFetching; + + let refetching = false; + const formInterceptor: SubmitFunction = async () => { + refetching = true; + return async ({ update, result }) => { + refetching = false; + if (result.type === "failure") { + if (result.data && result.data.errors.length > 0) { + for (const each of result.data.errors) { + toast.error(each.message); + } + } + } + const isSuccess = result.type === "success"; + await update({ reset: isSuccess }); + if (isSuccess) { + toast.success("Successfuly refetched distributors"); + $dQ.refetch(); + } + }; + }; +</script> + +<Title text={"Distributors"} size={"h2"} /> + +<div class="flex w-full justify-between items-center"> + {#if isLoading} + <Pill text={"Querying..."} theme={"slate"} /> + {:else if $dQ.data} + <Pill text={`${$dQ.data.enabled} enabled`} theme={"green"} /> + {/if} + <form + method="post" + action={"/admin/api-controls/?/refetchDistributors"} + use:enhance={formInterceptor} + > + <Button + disabled={refetching} + text={refetching ? "Refetching..." : "Refetch"} + intent={refetching ? "ghost" : "primary"} + otherOptions={{ type: "submit" }} + /> + </form> +</div> diff --git a/src/routes/admin/api-controls/session-verifier.svelte b/src/routes/admin/api-controls/session-verifier.svelte new file mode 100755 index 0000000..7cb0678 --- /dev/null +++ b/src/routes/admin/api-controls/session-verifier.svelte @@ -0,0 +1,66 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import type { trpc } from "$lib/trpc/trpc"; + import { CheckIcon } from "lucide-svelte"; + import SetNewSessionForm from "./set-new-session-form.svelte"; + + export let api: ReturnType<typeof trpc>; + let sOk = api.apiAuth.isApiSessionValid.createQuery( + { checkingUserSession: false }, + { refetchOnWindowFocus: false, refetchInterval: false, retry: 0 }, + ); + $: isLoading = $sOk.isLoading || $sOk.isFetching; + $: isApiSessionValid = !isLoading && $sOk.data?.valid === true; + + const onSuccessfulLogin = () => { + $sOk.refetch(); + }; + + let logoutUserM = api.apiAuth.logoutUser.createMutation({ + onSuccess: (d) => { + $sOk.refetch(); + }, + }); + + function logoutUser() { + $logoutUserM.mutateAsync({ userId: undefined }); + } + + function checkSession() { + $sOk.refetch(); + } +</script> + +<Title text={"Session"} size={"h2"} /> + +<div class="flex w-full items-center justify-between gap-2"> + {#if isLoading} + <Pill text={"Verifying..."} theme={"slate"} /> + {:else if isApiSessionValid} + <div class="items-center flex flex-row gap-2 w-full justify-between"> + <div + class="flex items-center gap-2 px-4 p-2 rounded-full bg-emerald-50 text-emerald-500 justify-center w-max" + > + Active + <CheckIcon class="w-6 h-auto" /> + </div> + + <div class="flex items-center gap-2"> + <Button + intent="primaryInverted" + onClick={() => checkSession()} + text={"Check"} + /> + <Button onClick={() => logoutUser()} text={"Logout"} /> + </div> + </div> + {:else} + <Pill text={"invalid"} theme={"rose"} /> + {/if} +</div> + +{#if !isLoading && !isApiSessionValid} + <SetNewSessionForm {api} {onSuccessfulLogin} /> +{/if} diff --git a/src/routes/admin/api-controls/set-new-session-form.svelte b/src/routes/admin/api-controls/set-new-session-form.svelte new file mode 100755 index 0000000..bfb0145 --- /dev/null +++ b/src/routes/admin/api-controls/set-new-session-form.svelte @@ -0,0 +1,110 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import type { TRPC } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import { onMount } from "svelte"; + + export let api: TRPC; + export let onSuccessfulLogin: () => void; + + let signingIn = false; + let captchaAnswer = ""; + + let getNewSessionM = api.apiAuth.getNewSession.createMutation({ + onSuccess: (o) => { + signingIn = false; + if (!o.success) { + for (const each of o.errors) { + toast.error(each.message); + } + return; + } + toast.success("Successfully authenticated", { + duration: 1000 * 10, + }); + onSuccessfulLogin(); + }, + onError: (e) => { + console.log(e); + signingIn = false; + toast.error( + "An error occured while authenticating, please try again later", + ); + }, + }); + + let captchaImage = ""; + let captchaId = ""; + + let captchaM = api.apiAuth.getCaptcha.createMutation({ + onSuccess: (d) => { + captchaId = d.id; + captchaImage = d.image; + }, + onError: (e) => { + toast.error( + "An error occured while loading captcha, please try again later", + ); + }, + }); + + $: fetchingCaptcha = $captchaM.isLoading; + + const onFormSubmit = async () => { + if (captchaAnswer.length !== 4) { + toast.error("Captcha answer must be 4 characters long"); + return; + } + if ($captchaM.data?.id) { + signingIn = true; + $getNewSessionM.mutateAsync({ + captchaId, + captchaAnswer, + }); + } else { + toast.error("Captcha not loaded, refresh and try again"); + } + }; + + onMount(() => { + $captchaM.mutateAsync(); + }); +</script> + +<form + class="flex flex-col gap-4 w-full" + on:submit|preventDefault={onFormSubmit} +> + <Title size={"h3"} text={"Login and get new session"} /> + <div class="gap-2 flex items-center"> + {#if !fetchingCaptcha} + <img + class="w-64 h-16 rounded-md" + src={`data:image/jpeg;base64,${captchaImage}`} + alt={"Captcha"} + /> + {:else} + <div class="w-64 h-16 rounded-md bg-gray-200 animate-pulse"></div> + {/if} + <input hidden name="captchaId" value={captchaId} /> + <Input + bind:value={captchaAnswer} + name={"captchaAnswer"} + placeholder={"Captcha Answer"} + otherInputOptions={{ + required: true, + minLength: 4, + maxLength: 4, + }} + /> + </div> + <Button + disabled={signingIn} + text={signingIn ? "Logging in..." : "Login"} + intent={signingIn ? "ghost" : "primary"} + fullwidth={"yes"} + otherOptions={{ type: "submit" }} + /> +</form> diff --git a/src/routes/admin/copy-counts.ls.ts b/src/routes/admin/copy-counts.ls.ts new file mode 100755 index 0000000..7728998 --- /dev/null +++ b/src/routes/admin/copy-counts.ls.ts @@ -0,0 +1,19 @@ +export type CopyCounts = Record<string, number>; + +const LS_KEY = "copy-counts"; + +export function getCopyCountsFromLS(): CopyCounts { + const copyCount = localStorage.getItem(LS_KEY); + if (copyCount === "undefined") { + localStorage.removeItem(LS_KEY); + return {}; + } + return !!copyCount ? JSON.parse(copyCount) : {}; +} + +export function setCopyCountsToLS(copyCount: CopyCounts) { + if (!copyCount) { + return; + } + localStorage.setItem(LS_KEY, JSON.stringify(copyCount)); +} diff --git a/src/routes/admin/data-fetching-filters.svelte b/src/routes/admin/data-fetching-filters.svelte new file mode 100755 index 0000000..0957479 --- /dev/null +++ b/src/routes/admin/data-fetching-filters.svelte @@ -0,0 +1,97 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import Line from "$lib/components/atoms/line.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + import type { Draw } from "$lib/utils/data.types"; + import toast from "svelte-french-toast"; + import { onMount } from "svelte"; + import { filters } from "./fs.stores"; + + export let fetchDataFn: () => Promise<void>; + export let showFilters: boolean; + export let draws: Draw[] = []; + export let fetchingFinalSheet: boolean = false; + + $: drawsCount = draws.length; + onMount(() => { + filters.update((f) => ({ ...f, draw: draws[0] })); + }); +</script> + +{#if drawsCount > 0 && showFilters} + <section class="flex flex-col md:flex-row gap-4 w-full"> + <Input + bind:value={$filters.date} + label={"Date"} + inputType={"date"} + placeholder={"Date"} + /> + <Select + fullWidth + label={"Draw"} + options={draws.map((e) => { + return { label: e.title, value: e.id, id: e.id }; + })} + onSelect={(e) => { + if (drawsCount > 0) { + filters.update((f) => ({ + ...f, + // @ts-ignore + draw: draws.find((d) => d.id === e.target.value), + })); + } + }} + /> + </section> + <div class="flex justify-end w-full"> + <Button + text={"Apply filters"} + disabled={fetchingFinalSheet} + onClick={async () => { + if ( + $filters.date.length < 1 || + !$filters.draw || + !$filters.draw.id + ) { + toast.error("Select all filters properly to fetch data"); + return; + } + showFilters = false; + toast("Fetching data for selected filters"); + await fetchDataFn(); + }} + /> + </div> +{:else if !showFilters} + <div + class="flex flex-col md:flex-row gap-4 items-center justify-between w-full" + > + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Data Filters:"} theme={"sky"} /> + <Pill text={$filters.date} theme={"slate"} /> + <Pill text={$filters.draw?.title} theme={"slate"} /> + </div> + + <Button + text={fetchingFinalSheet ? "Fetching" : "Refetch"} + intent={fetchingFinalSheet ? "ghost" : "primaryInverted"} + disabled={fetchingFinalSheet} + onClick={async () => { + if ( + $filters.date.length < 1 || + !$filters.draw || + !$filters.draw.id + ) { + toast.error("Select all filters properly to fetch data"); + return; + } + showFilters = false; + toast("Fetching data for selected filters"); + await fetchDataFn(); + }} + /> + </div> +{/if} +<Line /> diff --git a/src/routes/admin/fs-info.svelte b/src/routes/admin/fs-info.svelte new file mode 100755 index 0000000..0106be9 --- /dev/null +++ b/src/routes/admin/fs-info.svelte @@ -0,0 +1,36 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Line from "$lib/components/atoms/line.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import type { FSTotals } from "$lib/utils/data.types"; + + let showFSControls: boolean; + export let totals: FSTotals; +</script> + +{#if showFSControls} + <div class="flex flex-col w-full gap-2"> + <div class="flex justify-end"> + <Button + text={"Apply & close"} + onClick={() => { + showFSControls = !showFSControls; + }} + /> + </div> + </div> +{:else if !showFSControls} + <div class="w-full items-center flex justify-between"> + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Totals :"} theme={"sky"} /> + <Pill text={`F rate : ${totals.rate.first}`} theme={"slate"} /> + <Pill text={`F comm : ${totals.commission.first}`} theme={"slate"} /> + <Pill text={`F net : ${totals.netRate.first}`} theme={"slate"} /> + + <Pill text={`S rate : ${totals.rate.second}`} theme={"slate"} /> + <Pill text={`S comm : ${totals.commission.second}`} theme={"slate"} /> + <Pill text={`S net : ${totals.netRate.second}`} theme={"slate"} /> + </div> + </div> +{/if} +<Line /> diff --git a/src/routes/admin/fs-modal.svelte b/src/routes/admin/fs-modal.svelte new file mode 100755 index 0000000..1d4cae6 --- /dev/null +++ b/src/routes/admin/fs-modal.svelte @@ -0,0 +1,591 @@ +<script lang="ts"> + import Input from "$lib/components/atoms/input.svelte"; + import clsx from "clsx"; + import { createDialog } from "@melt-ui/svelte"; + import IconButton from "$lib/components/atoms/icon-button.svelte"; + import IconX from "~icons/bi/x"; + import Title from "$lib/components/atoms/title.svelte"; + import { Button } from "$lib/components/ui/button"; + import Line from "$lib/components/atoms/line.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Switch from "$lib/components/atoms/switch.svelte"; + import IconPhClipboardTextLight from "~icons/ph/clipboard-text-light"; + import toast from "svelte-french-toast"; + import type { CopyCounts } from "./copy-counts.ls"; + import { filters } from "./fs.stores"; + import { nowDT, postdata } from "./stores"; + import type { TRPC } from "$lib/trpc/trpc"; + import { hasDrawBeenClosed } from "$lib/utils"; + import PostDataSummarySection from "./post-data-summary-section.svelte"; + import * as AlertDialog from "$lib/components/ui/alert-dialog/index"; + + export let api: TRPC; + export let controllerState: Record<string, any>; + export let onControllerStateApply: (state: Record<string, any>) => void; + export let copyCounts: CopyCounts; + const trackingLexiCodeNumbersKeys = ["ab", "abc"]; + export let isChangingControllerState: boolean; + let previewData = false; + + let hasAlreadyPostedQ = api.postData.hasPosted.createQuery( + { date: $filters.date, draw: $filters.draw! }, + { refetchOnWindowFocus: false, retry: 3 }, + ); + $: hasAlreadyPosted = $hasAlreadyPostedQ.data + ? $hasAlreadyPostedQ.data.hasPosted + : false; + + // $: hasTimePassed = hasDrawBeenClosed($filters.date, $filters.draw!, $nowDT); + $: hasTimePassed = false; + + let postUsersQ = api.apiUser.getAllPostUsers.createQuery(undefined, { + refetchOnWindowFocus: false, + }); + $: postUsers = + !$postUsersQ.isLoading && !!$postUsersQ.data ? $postUsersQ.data : []; + + let postDataM = api.postData.post.createMutation({ + onSuccess: (d) => { + console.log(d); + if (!d.ok) { + if (!!d.detail && d.detail.length > 0) { + toast.error(d.detail); + return; + } + // @ts-ignore + for (const each of d.errors) { + toast.error(each.message); + } + return; + } + $hasAlreadyPostedQ.refetch(); + toast.success("Data posted successfully"); + }, + onError: (e) => { + console.error(e); + toast.error("Error posting data"); + }, + }); + + $: posting = $postDataM.isLoading; + + function beginPostingData( + e: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }, + ) { + // @ts-ignore + const fd = new FormData(e.target); + const data = {} as Record<string, any>; + for (const [key, value] of fd.entries()) data[key] = value; + toast("Starting to post data, please wait..."); + $postDataM.mutateAsync({ + yes: { + customData: "", + date: $filters.date, + draw: $filters.draw, + minPrize: +controllerState.minPrize, + maxPrize: +controllerState.maxPrize, + twoDigitRates: { + first: $filters.draw?.abRateF ?? 0, + second: $filters.draw?.abRateS ?? 0, + }, + threeDigitRates: { + first: $filters.draw?.abcRateF ?? 0, + second: $filters.draw?.abcRateS ?? 0, + }, + }, + data: $postdata, + }); + } + + function triggerPostForm() { + const form = document.getElementById("post-data-form"); + if (form) { + form.dispatchEvent(new Event("submit")); + } + } + + let refreshingPayload = false; + let payloadForPostingData = { + date: $filters.date, + draw: $filters.draw, + minPrize: +controllerState.minPrize, + maxPrize: +controllerState.maxPrize, + }; + + function startPreviewingData() { + reRenderPreviewPostData(); + previewData = true; + } + + function reRenderPreviewPostData() { + refreshingPayload = true; + console.log($filters.draw); + payloadForPostingData = { + date: $filters.date, + draw: $filters.draw, + minPrize: +controllerState.minPrize, + maxPrize: +controllerState.maxPrize, + }; + + console.log(`The posting payload :: `, payloadForPostingData); + setTimeout(() => { + refreshingPayload = false; + }, 100); + } + + const getParsedMessageFromMatchingNumbers = ( + lexiCode: string | null, + _copyCount: number = 1, + ) => { + let out = ""; + if (lexiCode) { + // @ts-ignore + for (const each of controllerState[lexiCode]) { + out += `${each}.`; + } + } else { + for (const lc of ["abc", "+abc", "a+bc", "ab+c"]) { + // @ts-ignore + for (const each of controllerState[lc]) { + out += `${each}.`; + } + } + } + if (_copyCount > 1) { + let out2 = ""; + for (let i = 0; i < _copyCount; i++) { + out2 += out; + } + out = out2; + } + // remove the last dot + return out.substring(0, out.length - 1); + }; + + const doesControllerStateValueHaveValidSize = (key: string): boolean => { + // @ts-ignore + const item = controllerState[key]; + if (item.size === undefined) return false; + return item.size > 0; + }; + + const getSize = (key: string) => { + // @ts-ignore + const item = controllerState[key]; + return item.size ?? 0; + }; + + function getSizeRemovingNumbersWithRepeatingDigits(key: string) { + // @ts-ignore + const item = controllerState[key]; + const out = new Set<string>(); + for (const each of item) { + if (each.length === new Set(each).size) { + out.add(each); + } + } + return out.size; + } + + const getIterator = (key: string) => { + // @ts-ignore + const item = controllerState[key]; + return item as Set<string>; + }; + + const { + trigger, + portal, + overlay, + content, + close: closeModal, + open: modalVisible, + } = createDialog(); +</script> + +<div> + <button {...$trigger} use:trigger> + <Button>Open Controller</Button> + </button> + <div use:portal> + {#if $modalVisible} + <div {...$overlay} class="fixed inset-0 z-20 bg-black/50" /> + <div + class={clsx( + "fixed left-[50%] top-[50%] z-30 max-h-[90vh] w-[90vw] max-w-5xl translate-x-[-50%] translate-y-[-50%] rounded-md bg-white p-8 shadow-lg overflow-y-auto", + )} + {...$content} + use:content + id="fs-controller-modal" + > + <div class="flex flex-col gap-4 w-full"> + <div class="flex gap-4 justify-between w-full"> + <Title size={"h2"} text={"Filter rows"} /> + <button {...$closeModal} use:closeModal> + <IconButton + icon={IconX} + color={"gray"} + size={"6"} + /> + </button> + </div> + + <div class="flex gap-4 justify-between"> + <Input + label={"Min Prize"} + inputType={"number"} + bind:value={controllerState.minPrize} + onInput={(e) => { + // @ts-ignore + const val = e.target.value; + controllerState.minPrize = val; + }} + /> + <Input + label={"Max Prize"} + inputType={"number"} + bind:value={controllerState.maxPrize} + onInput={(e) => { + // @ts-ignore + const val = e.target.value; + controllerState.maxPrize = val; + }} + /> + </div> + <Switch + label={"Filter rows where abcd type-numbers are not booked"} + checked={controllerState.filterOnlyRowsWithNo4DigitsBooked} + onChange={(v) => { + controllerState.filterOnlyRowsWithNo4DigitsBooked = + v; + }} + /> + + <div + class="flex flex-col sm:flex-row gap-2 justify-end w-full" + > + <Button on:click={onControllerStateApply}> + Apply changes + </Button> + <button {...$closeModal} use:closeModal> + <Button + variant={"primaryInverted"} + on:click={onControllerStateApply} + > + Apply changes & close + </Button> + </button> + </div> + + {#if (controllerState.minPrize > 0 || controllerState.maxPrize > 0) && !isChangingControllerState} + <form + on:submit|preventDefault={beginPostingData} + class="flex flex-col gap-6 w-full" + id="post-data-form" + > + <div class="w-full h-0.5 border-t-2"></div> + <Title + size={"h3"} + text={`Post Data (${new Date($filters.date).toDateString()} - ${$filters.draw?.title})`} + /> + + <div class="flex flex-col md:flex-row gap-6"> + <div + class="flex gap-4 w-full flex-col md:flex-row" + > + <Input + label={"2 Digit rate (F)"} + name="abRateF" + inputType={"number"} + padding="sm" + otherInputOptions={{ min: 0 }} + value={$filters.draw?.abRateF} + onInput={(e) => { + filters.update((f) => { + // @ts-ignore + f.draw.abRateF = + // @ts-ignore + +e.target.value; + return f; + }); + }} + /> + <Input + label={"2 Digit rate (S)"} + name="abRateS" + inputType={"number"} + padding="sm" + otherInputOptions={{ min: 0 }} + value={$filters.draw?.abRateS} + onInput={(e) => { + filters.update((f) => { + // @ts-ignore + f.draw.abRateS = + // @ts-ignore + +e.target.value; + return f; + }); + }} + /> + </div> + + <div + class="flex gap-4 w-full flex-col md:flex-row" + > + <Input + label={"3 Digit rate (F)"} + name="abcRateF" + inputType={"number"} + otherInputOptions={{ min: 0 }} + value={$filters.draw?.abcRateF} + padding="sm" + onInput={(e) => { + filters.update((f) => { + // @ts-ignore + f.draw.abcRateF = + // @ts-ignore + +e.target.value; + return f; + }); + }} + /> + <Input + value={$filters.draw?.abcRateS} + name="abcRateS" + label={"3 Digit rate (S)"} + inputType={"number"} + padding="sm" + otherInputOptions={{ min: 0 }} + onInput={(e) => { + filters.update((f) => { + // @ts-ignore + f.draw.abcRateS = + // @ts-ignore + +e.target.value; + return f; + }); + }} + /> + </div> + </div> + + <Switch + label={"While posting data, filter numbers with repeating digits"} + checked={$filters.draw + ?.filterDuplicatesWhilePosting} + onChange={(checked) => { + filters.update((f) => { + // @ts-ignore + f.draw.filterDuplicatesWhilePosting = + checked; + return f; + }); + }} + /> + + <div + class="flex gap-2 items-center flex-col md:flex-row flex-wrap" + > + <div class="w-max"> + <Pill + text={`ABC | All - (${getSize("abc")}) | Non-repeating - (${getSizeRemovingNumbersWithRepeatingDigits("abc")})`} + theme={"darkSky"} + /> + </div> + + <div class="w-max"> + <Pill + text={`AB | All - (${getSize("ab")}) | Non-repeating - (${getSizeRemovingNumbersWithRepeatingDigits("ab")})`} + theme={"darkSky"} + /> + </div> + <div class="w-max"> + <Pill + text={`Can post data for ${postUsers.length} users`} + theme={"slate"} + /> + </div> + + {#if hasAlreadyPosted} + <div class="w-max"> + <Pill + text={`Already posted for this draw`} + theme={"green"} + /> + </div> + {/if} + </div> + + <div + class="flex gap-8 items-center justify-between" + > + {#if !hasAlreadyPosted || posting} + <Button + disabled={!!hasTimePassed || posting} + type="submit" + > + {hasTimePassed + ? "Time has passed for this draw" + : posting + ? "Posting data..." + : "Begin posting data for draw"} + </Button> + {:else} + <AlertDialog.Root> + <AlertDialog.Trigger type="button"> + <Button + disabled={!!hasTimePassed || + posting} + type="button" + > + Post data for draw + </Button> + </AlertDialog.Trigger> + <AlertDialog.Content> + <AlertDialog.Header> + <AlertDialog.Title> + Post data again + </AlertDialog.Title> + <AlertDialog.Description> + Are you sure you want to + post data again for this + draw? This will post more + entries on top of the + existing ones. + </AlertDialog.Description> + </AlertDialog.Header> + <AlertDialog.Footer> + <AlertDialog.Cancel + type="button" + > + No, cancel + </AlertDialog.Cancel> + <AlertDialog.Action + on:click={triggerPostForm} + > + Yes, start posting data + </AlertDialog.Action> + </AlertDialog.Footer> + </AlertDialog.Content> + </AlertDialog.Root> + {/if} + + {#if previewData} + <Button + variant={"secondary"} + on:click={reRenderPreviewPostData} + > + Regenerate Preview Data + </Button> + {:else} + <Button + variant={"primaryInverted"} + on:click={startPreviewingData} + > + Preview data + </Button> + {/if} + </div> + </form> + + {#if !refreshingPayload && previewData} + <div class="w-full h-0.5 border-t-2"></div> + <Title size={"h3"} text={`Data Summary Preview`} /> + <PostDataSummarySection + {api} + bind:payload={payloadForPostingData} + /> + {/if} + {/if} + + {#if controllerState.matchingNumbers.size > 0 && !isChangingControllerState} + <Line /> + <Title size={"h3"} text={"Matching numbers"} /> + <div class="w-full max-h-[40vh] overflow-y-auto p-2"> + {#each ["abc", ...trackingLexiCodeNumbersKeys.filter((e) => e !== "abc")] as key} + {#if doesControllerStateValueHaveValidSize(key)} + <div + class="w-full flex flex-col gap-4 my-2" + > + <Title + size={"h4"} + text={`${key.toUpperCase()} | All - (${getSize(key)}) | Non-repeating - (${getSizeRemovingNumbersWithRepeatingDigits(key)})`} + /> + <div class="flex flex-wrap gap-2"> + {#each getIterator(key) as item} + <Pill + smallerText + text={item} + theme={"slate"} + /> + {/each} + </div> + <Line /> + </div> + {/if} + {/each} + </div> + <Line /> + {#each [null, "ab", "abc"] as lc} + <div + class="w-full flex gap-4 items-center justify-between" + > + <Title + size={"h3"} + text={(lc !== null + ? lc.toUpperCase() + : "") + + " Message" + + (!lc + ? "" + : ` | (${getSize(lc)}) | (${getSizeRemovingNumbersWithRepeatingDigits(lc)})`)} + /> + <div class="w-full max-w-xs"> + <Input + value={copyCounts + ? (copyCounts[lc ?? "message"] ?? + "") + : ""} + label={"Copy count"} + placeholder={`For ${lc ?? ""} message`} + onInput={(e) => { + // @ts-ignore + const val = e.target.value; + const key = lc ?? "message"; + copyCounts[key] = val; + }} + /> + </div> + </div> + <div class="relative"> + <textarea + class="w-full h-48 p-2 outline-none bg-sky-50 border-2 border-sky-500 text-sky-600 rounded-md" + value={getParsedMessageFromMatchingNumbers( + lc, + )} + /> + <button + class="absolute top-2 right-5 p-2 rounded-md border bg-emerald-100 text-emerald-600 border-emerald-600 cursor-pointer shadow-md transition-colors duration-100 hover:bg-emerald-200" + aria-label="Copy to clipboard" + on:click={() => { + navigator.clipboard.writeText( + getParsedMessageFromMatchingNumbers( + lc, + copyCounts[lc ?? "message"], + ), + ); + toast.success( + "Message copied to clipboard", + ); + }} + > + <IconPhClipboardTextLight /> + </button> + </div> + {/each} + {/if} + </div> + </div> + {/if} + </div> +</div> diff --git a/src/routes/admin/fs-table.svelte b/src/routes/admin/fs-table.svelte new file mode 100755 index 0000000..8602521 --- /dev/null +++ b/src/routes/admin/fs-table.svelte @@ -0,0 +1,426 @@ +<script lang="ts"> + import Input from "$lib/components/atoms/input.svelte"; + import type { ReducedFinalSheetRow } from "$lib/utils/data.types"; + import { fsTableOptions } from "./fs.stores"; + import { + flexRender, + createColumnHelper, + createSvelteTable, + type SortDirection, + } from "@tanstack/svelte-table"; + import Pagination from "$lib/components/atoms/pagination.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + import clsx from "clsx"; + import CenteredSpinner from "$lib/components/molecules/centered-spinner.svelte"; + import { getAllMatchingChildNumbersObject } from "$lib/utils/finalsheet.utils"; + import { + setCopyCountsToLS, + getCopyCountsFromLS, + type CopyCounts, + } from "./copy-counts.ls"; + import FsModal from "./fs-modal.svelte"; + import type { TRPC } from "$lib/trpc/trpc"; + + export let api: TRPC; + export let rowCount: number; + export let data: ReducedFinalSheetRow[]; + let copyCounts: CopyCounts = {}; + let copyCountsUpdaterTimeout: NodeJS.Timeout; + $: { + copyCountsUpdaterTimeout = setTimeout(() => { + setCopyCountsToLS(copyCounts); + }, 1000); + } + // export let fetchFSRow: (num: string) => Promise<void>; + let affectingRowVisibility = false; + type MatchingNumbers = Set<string>; + const trackingLexiCodeNumbersKeys = [ + "a", + "ab", + "abc", + "+abc", + "a+bc", + "ab+c", + ]; + let isChangingControllerState = false; + let controllerState = { + minPrize: 0, + maxPrize: 0, + filterOnlyRowsWithNo4DigitsBooked: false, + matchingNumbers: new Set() as MatchingNumbers, + a: new Set() as MatchingNumbers, + ab: new Set() as MatchingNumbers, + abc: new Set() as MatchingNumbers, + "+abc": new Set() as MatchingNumbers, + "a+bc": new Set() as MatchingNumbers, + "ab+c": new Set() as MatchingNumbers, + }; + let sheetType: "first" | "second" = "first"; + let updatingPaginationState = false; + let originalRowCount = 0; + const minPageSize = 100; + + let pagination = { pageSize: minPageSize, pageIndex: 0 }; + let globalFilter = ""; + + const getParsedMessageFromMatchingNumbers = ( + lexiCode: string | null, + _copyCount: number = 1, + ) => { + let out = ""; + if (lexiCode) { + // @ts-ignore + for (const each of controllerState[lexiCode]) { + out += `${each}.`; + } + } else { + for (const lc of ["abc", "+abc", "a+bc", "ab+c"]) { + // @ts-ignore + for (const each of controllerState[lc]) { + out += `${each}.`; + } + } + } + if (_copyCount > 1) { + let out2 = ""; + for (let i = 0; i < _copyCount; i++) { + out2 += out; + } + out = out2; + } + // remove the last dot + return out.substring(0, out.length - 1); + }; + + const onControllerStateApply = () => { + affectingRowVisibility = true; + updatingPaginationState = true; + isChangingControllerState = true; + let matching = [] as ReducedFinalSheetRow[]; + const min = Number(controllerState.minPrize); + const max = Number(controllerState.maxPrize); + controllerState.matchingNumbers.clear(); + controllerState.a.clear(); + controllerState.ab.clear(); + controllerState.abc.clear(); + controllerState["+abc"].clear(); + controllerState["a+bc"].clear(); + controllerState["ab+c"].clear(); + if (!isNaN(min) && !isNaN(max)) { + for (const row of data) { + if ( + min < 0 || + max < 0 || + (row.prize[sheetType] >= min && row.prize[sheetType] <= max) + ) { + if ( + controllerState.filterOnlyRowsWithNo4DigitsBooked && + row.frequencies.abcd[sheetType] > 0 + ) { + continue; + } + matching.push(row); + controllerState.matchingNumbers.add(row.number); + const found = getAllMatchingChildNumbersObject(row.number); + for (const lexiCode of trackingLexiCodeNumbersKeys) { + // @ts-ignore + // if (row.frequencies[lexiCode][sheetType] > 0) { + // @ts-ignore + controllerState[lexiCode].add(found[lexiCode]); + // } + } + } + } + } else { + matching = controllerState.filterOnlyRowsWithNo4DigitsBooked + ? data.filter((r) => r.frequencies.abcd[sheetType] === 0) + : data; + } + rowCount = matching.length; + fsTableOptions.update((old) => ({ + ...old, + data: matching, + state: { + ...old.state, + // @ts-ignore + pagination: { ...old.state?.pagination, pageIndex: 0 }, + }, + })); + controllerState = controllerState; + setTimeout(() => { + updatingPaginationState = false; + affectingRowVisibility = false; + isChangingControllerState = false; + }, 100); + }; + + const getSortingSymbol = (isSorted: boolean | SortDirection) => { + return isSorted === "asc" ? "▲" : isSorted === "desc" ? "▼" : ""; + }; + + const setCurrentPage = (page: number) => { + fsTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageIndex: page, + pageSize: state.state?.pagination?.pageSize ?? minPageSize, + }, + }, + })); + }; + + const setPaginationPageSize = (size: number) => { + updatingPaginationState = true; + setCurrentPage(0); + fsTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageSize: size, + pageIndex: state.state?.pagination?.pageIndex ?? 0, + }, + }, + })); + setTimeout(() => { + updatingPaginationState = false; + }, 100); + }; + + const setGlobalFilter = (filter: string) => { + setPaginationPageSize($table.getState().pagination.pageSize); + globalFilter = filter; + fsTableOptions.update((p) => { + return { ...p, state: { ...p.state } }; + }); + rowCount = $table.getFilteredRowModel().rows.length; + }; + + const colHelper = createColumnHelper<ReducedFinalSheetRow>(); + const columns = [ + colHelper.display({ + id: "number", + cell: (info) => info.row.original.number, + header: "No", + // @ts-ignore + accessorFn: (r) => r.number.toString(), + }), + colHelper.display({ + id: "frequency", + cell: (info) => info.row.original.frequency[sheetType], + header: "Booked Qt.", + // @ts-ignore + accessorFn: (r) => r.frequency[sheetType].toString(), + }), + + colHelper.display({ + id: "rate", + cell: (info) => info.row.original.rate[sheetType], + header: "Rate", + // @ts-ignore + accessorFn: (r) => r.rate[sheetType].toString(), + }), + colHelper.display({ + id: "prize", + cell: (info) => info.row.original.prize[sheetType], + header: "Prize", + // @ts-ignore + accessorFn: (r) => r.prize[sheetType].toString(), + }), + colHelper.display({ + id: "profit", + cell: (info) => info.row.original.profit[sheetType], + header: "Profit", + // @ts-ignore + accessorFn: (r) => r.profit[sheetType].toString(), + sortingFn: (a, b) => { + return a.original.profit[sheetType] < + b.original.profit[sheetType] + ? -1 + : 1; + }, + }), + ]; + + const table = createSvelteTable(fsTableOptions); + + const typeAnnotateComponent = (a: any) => { + return a as any; + }; + + let searchTimeOut: NodeJS.Timeout; + const handleEntrySearch = (e: any) => { + clearTimeout(searchTimeOut); + searchTimeOut = setTimeout(() => { + fsTableOptions.update((opts: any) => { + return { + ...opts, + state: { + ...opts.state, + columnFilters: [ + { id: "number", value: e.target.value }, + ], + }, + }; + }); + }, 300); + }; + + onMount(() => { + fsTableOptions.update((p: any) => { + return { + ...p, + columns, + state: { + ...p.state, + pagination, + columnFilters: [], + }, + }; + }); + originalRowCount = rowCount; + copyCounts = getCopyCountsFromLS(); + }); +</script> + +<!-- INFO: The modal --> +<FsModal + {api} + bind:controllerState + bind:copyCounts + bind:isChangingControllerState + {onControllerStateApply} +/> + +<!-- INFO: The table itself --> +<div class="w-full text-black"> + <div class="my-2 flex w-full justify-between items-center"> + <div + class="flex justify-between flex-col w-full md:flex-row md:max-w-xs gap-2" + > + <Select + fullWidth + label={"Row count"} + options={[100, 250, 500, 1000].map((n) => { + return { + id: n.toString(), + label: n.toString(), + value: n.toString(), + }; + })} + onSelect={(e) => { + // @ts-ignore + const chosen = Number(e.target.value); + setPaginationPageSize(chosen); + }} + /> + <Select + fullWidth + label={"Sheet Type"} + options={[ + { label: "First", value: "first", id: 1 }, + { label: "Second", value: "second", id: 2 }, + ]} + onSelect={(e) => { + affectingRowVisibility = true; + // @ts-ignore + const chosen = e.target.value; + sheetType = chosen; + setTimeout(() => { + affectingRowVisibility = false; + }, 100); + }} + /> + </div> + <div class="flex justify-end max-w-xs"> + <Input + placeholder={"Search"} + fieldWidth={"full"} + onInput={handleEntrySearch} + /> + </div> + </div> + + {#if affectingRowVisibility} + <CenteredSpinner /> + {:else} + <div class="w-full overflow-y-auto h-[35.7rem]"> + <table class="w-full"> + <thead> + {#each $table.getHeaderGroups() as headerGroup} + <tr> + {#each headerGroup.headers as header} + <th + class="p-2 px-4 border sticky top-0 bg-sky-50 text-sky-700" + colSpan={header.colSpan} + > + {#if !header.isPlaceholder} + <button + class={clsx( + "flex outline-none justify-center items-center gap-1 w-full h-full", + )} + disabled={!header.column.getCanSort()} + on:click={header.column.getToggleSortingHandler()} + > + <svelte:component + this={typeAnnotateComponent( + flexRender( + header.column.columnDef + .header, + header.getContext(), + ), + )} + /> + <span + >{getSortingSymbol( + header.column.getIsSorted(), + )}</span + > + </button> + {/if} + </th> + {/each} + </tr> + {/each} + </thead> + <tbody> + {#each $table.getRowModel().rows as row} + <tr + class="cursor-pointer hover:bg-slate-50 transition-colors duration-100" + on:click={() => { + // fetchFSRow(row.original.number); + }} + > + {#each row.getVisibleCells() as cell} + <td class="p-1 px-4 border text-center"> + <svelte:component + this={typeAnnotateComponent( + flexRender( + cell.column.columnDef.cell, + cell.getContext(), + ), + )} + /> + </td> + {/each} + </tr> + {/each} + </tbody> + </table> + </div> + {/if} + + {#if !updatingPaginationState} + <div class="w-full py-2"> + <Pagination + count={rowCount} + page={$table.getState().pagination.pageIndex + 1} + perPage={$table.getState().pagination.pageSize} + siblingCount={1} + setPage={setCurrentPage} + /> + </div> + {/if} +</div> diff --git a/src/routes/admin/fs.stores.ts b/src/routes/admin/fs.stores.ts new file mode 100755 index 0000000..9bee476 --- /dev/null +++ b/src/routes/admin/fs.stores.ts @@ -0,0 +1,28 @@ +import { writable } from "svelte/store"; +import type { Draw, ReducedFinalSheetRow } from "$lib/utils/data.types"; +import { + type TableOptions, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, +} from "@tanstack/svelte-table"; + +export const fsTableOptions = writable<TableOptions<ReducedFinalSheetRow>>({ + data: [], + columns: [], + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableGlobalFilter: false, +}); + +export const setFSTableData = (data: ReducedFinalSheetRow[]) => { + fsTableOptions.update((state) => ({ ...state, data })); +}; + +export const filters = writable<{ date: string; draw: Draw | undefined }>({ + date: new Date().toISOString().split("T")[0], + draw: undefined, +}); diff --git a/src/routes/admin/post-data-config/+page.svelte b/src/routes/admin/post-data-config/+page.svelte new file mode 100644 index 0000000..0eaf32b --- /dev/null +++ b/src/routes/admin/post-data-config/+page.svelte @@ -0,0 +1,97 @@ +<script lang="ts"> + import { trpc } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import DataFetchConfig from "./data-fetch-config.svelte"; + import type { Draw } from "$lib/utils/data.types"; + import Loader from "$lib/components/molecules/loader.svelte"; + import AddPostUserModal from "./add-post-user-modal.svelte"; + import PostUserControls from "./post-user-controls.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + + const api = trpc(); + + let drawsQ = api.draw.getAllDraws.createQuery(undefined, { + refetchOnWindowFocus: false, + retry: 3, + }); + $: isLoading = $drawsQ.isLoading || $drawsQ.isFetching; + + let updateDrawFilterM = api.draw.savePresetInfoForDraws.createMutation({ + onSuccess: (d) => { + console.log(d); + toast("Saved successfully."); + }, + onError: (e) => { + console.error(e); + toast( + "An error occurred while fetching data. Try again after a page refresh.", + ); + }, + }); + + function updateDrawFilter(draws: Draw[]) { + $updateDrawFilterM.mutateAsync({ draws }); + } + + let dealersQ = api.apiUser.getAllDealersPostUserFormat.createQuery( + undefined, + { + refetchOnWindowFocus: false, + retry: 3, + }, + ); + $: dealersloading = $dealersQ.isLoading || $dealersQ.isFetching; + + function refreshUserList() { + $dealersQ.refetch(); + } +</script> + +<div class="w-full grid place-items-center p-4"> + {#if isLoading} + <div class="w-full h-[80vh] grid place-items-center"> + <Loader color="sky" /> + </div> + {:else} + <div class="w-full flex flex-col gap-4 max-w-8xl p-2 sm:p-4"> + <div class="flex flex-col-reverse lg:grid gap-8 lg:grid-cols-3"> + <!-- the draw presets list/grid --> + <div class="col-span-2"> + <DataFetchConfig + draws={$drawsQ.data} + fetching={false} + {updateDrawFilter} + /> + </div> + + <!-- the user list --> + <div class="col-span-1 w-full flex flex-col gap-8"> + <div + class="flex gap-4 flex-row justify-between items-center w-full" + > + <Title text="Users" size="h2" /> + {#if !dealersloading && !!$dealersQ.data} + <AddPostUserModal + users={$dealersQ.data ?? []} + {api} + {refreshUserList} + /> + {/if} + </div> + + {#if dealersloading} + <div class="w-full h-[80vh] grid place-items-center"> + <Loader color="sky" /> + </div> + {:else} + {#each $dealersQ.data ?? [] as user} + {#if user.postData} + <PostUserControls {api} {user} /> + {/if} + {/each} + {/if} + </div> + </div> + </div> + {/if} +</div> diff --git a/src/routes/admin/post-data-config/add-post-user-modal.svelte b/src/routes/admin/post-data-config/add-post-user-modal.svelte new file mode 100644 index 0000000..0e675cf --- /dev/null +++ b/src/routes/admin/post-data-config/add-post-user-modal.svelte @@ -0,0 +1,102 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import * as Dialog from "$lib/components/ui/dialog"; + import { buttonVariants } from "$lib/components/ui/button"; + import type { ApiPostUser } from "$lib/utils/data.types"; + import clsx from "clsx"; + import Input from "$lib/components/atoms/input.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import type { TRPC } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import { cn } from "$lib/utils"; + + export let api: TRPC; + export let refreshUserList: () => void; + export let users: ApiPostUser[]; + + let query = ""; + let changedUsersIds: Set<string> = new Set(); + + let saveChangesM = api.apiUser.setPostDataFlagForUser.createMutation({ + onSuccess: () => { + toast.success("Changes saved successfully"); + changedUsersIds = new Set(); + refreshUserList(); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + $: renderUsers = users.filter((u) => { + if (query === "") return true; + return ( + u.userName.toLowerCase().includes(query) || + u.userId.toLowerCase().includes(query) + ); + }); + + function onSave() { + const usersToPostData = users.filter((u) => + changedUsersIds.has(u.userId), + ); + console.log(usersToPostData); + if (usersToPostData.length < 1) { + return; + } + $saveChangesM.mutateAsync({ users: usersToPostData }); + } +</script> + +<Dialog.Root> + <Dialog.Trigger class={cn(buttonVariants(), "w-max")}> + Manager Users + </Dialog.Trigger> + <Dialog.Content class="sm:max-w-[425px]"> + <Dialog.Header> + <Title text="Select Users" size="h3" /> + <Dialog.Description> + Select the user with whom you want to post data with + </Dialog.Description> + </Dialog.Header> + + <Input placeholder="Search User" bind:value={query} /> + + <div class="grid gap-2 overflow-y-auto max-h-72 border rounded-md p-2"> + {#each renderUsers as user} + <button + class={clsx( + "rounded-md hover:bg-sky-200 text-sky-800 font-medium px-2 py-1 cursor-pointer transition-colors duration-100 border capitalize", + user.postData + ? "bg-sky-300 border-sky-600" + : "bg-sky-50 border-sky-200", + )} + on:click={() => { + user.postData = !user.postData; + if ( + !changedUsersIds.has(user.userId) || + user.postData + ) { + changedUsersIds.add(user.userId); + } else if ( + !!user.postData && + changedUsersIds.has(user.userId) + ) { + changedUsersIds.delete(user.userId); + } + }} + > + {user?.userName} - {user?.userId} + </button> + {/each} + </div> + <Dialog.Footer> + <Button + otherOptions={{ type: "submit" }} + text="Save" + intent="primary" + onClick={onSave} + /> + </Dialog.Footer> + </Dialog.Content> +</Dialog.Root> diff --git a/src/routes/admin/post-data-config/data-fetch-config.svelte b/src/routes/admin/post-data-config/data-fetch-config.svelte new file mode 100644 index 0000000..9adf7cf --- /dev/null +++ b/src/routes/admin/post-data-config/data-fetch-config.svelte @@ -0,0 +1,86 @@ +<script lang="ts"> + import { Button } from "$lib/components/ui/button"; + import { Input } from "$lib/components/ui/input"; + import { Label } from "$lib/components/ui/label"; + import Title from "$lib/components/atoms/title.svelte"; + import type { Draw } from "$lib/utils/data.types"; + import Switch from "$lib/components/atoms/switch.svelte"; + import { filters } from "./stores"; + + export let updateDrawFilter: (draws: Draw[]) => void; + export let draws: Draw[] = []; + export let fetching: boolean = false; + + function saveDrawFilterChanges() { + console.log(draws); + updateDrawFilter(draws); + } + + onMount(() => { + filters.update((d) => ({ ...d, draw: draws[0] })); + }); +</script> + +<div class="flex flex-col w-full gap-8"> + <div class="flex justify-between items-center gap-4 flex-row"> + <Title text="Draw Presets" size="h2" /> + + <Button disabled={fetching} on:click={() => saveDrawFilterChanges()}> + Save all changes + </Button> + </div> + + <div + class="w-full flex flex-col gap-4 lg:grid md:grid-cols-2 2xl:grid-cols-3" + > + {#each draws as draw} + <div class="w-full flex flex-col gap-4 p-4 rounded-md border"> + <Title text={draw.title} size="h4" /> + + <div class="flex gap-4 w-full flex-col md:flex-row"> + <div class="w-full space-y-2"> + <Label>2 Digit rate (F)</Label> + <Input + bind:value={draw.abRateF} + type={"number"} + min={0} + /> + </div> + <div class="w-full space-y-2"> + <Label>2 Digit rate (S)</Label> + <Input + bind:value={draw.abRateS} + type={"number"} + min={0} + /> + </div> + </div> + + <div class="flex gap-4 w-full flex-col md:flex-row"> + <div class="w-full space-y-2"> + <Label>3 Digit rate (F)</Label> + <Input + bind:value={draw.abcRateF} + type={"number"} + min={0} + /> + </div> + <div class="w-full space-y-2"> + <Label>3 Digit rate (S)</Label> + <Input + bind:value={draw.abcRateS} + type={"number"} + min={0} + /> + </div> + </div> + + <Switch + label={"Remove numbers that have repeating digits"} + bind:checked={draw.filterDuplicatesWhilePosting} + /> + <div class="w-full my-4"></div> + </div> + {/each} + </div> +</div> diff --git a/src/routes/admin/post-data-config/post-user-controls.svelte b/src/routes/admin/post-data-config/post-user-controls.svelte new file mode 100644 index 0000000..b00c1dd --- /dev/null +++ b/src/routes/admin/post-data-config/post-user-controls.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import Title from "$lib/components/atoms/title.svelte"; + import type { TRPC } from "$lib/trpc/trpc"; + import type { ApiPostUser } from "$lib/utils/data.types"; + import { UserIcon } from "lucide-svelte"; + import SessionVerifier from "./session-verifier.svelte"; + + export let user: ApiPostUser; + export let api: TRPC; +</script> + +<div + class="flex flex-col gap-4 p-4 py-6 md:p-6 items-center w-full border-2 rounded-lg" +> + <div + class="grid place-items-center p-4 rounded-full bg-indigo-50 text-indigo-600 w-max" + > + <UserIcon class="w-8 h-auto" /> + </div> + <Title text={`${user.userName} - ${user.userId}`} size={"h3"} /> + + <SessionVerifier {api} {user} /> +</div> diff --git a/src/routes/admin/post-data-config/session-verifier.svelte b/src/routes/admin/post-data-config/session-verifier.svelte new file mode 100755 index 0000000..6f382ca --- /dev/null +++ b/src/routes/admin/post-data-config/session-verifier.svelte @@ -0,0 +1,72 @@ +<script lang="ts"> + import Pill from "$lib/components/atoms/pill.svelte"; + import type { trpc } from "$lib/trpc/trpc"; + import { CheckIcon, LogOutIcon } from "lucide-svelte"; + import SetNewSessionForm from "./set-new-session-form.svelte"; + import type { ApiPostUser } from "$lib/utils/data.types"; + import { Button } from "$lib/components/ui/button"; + + export let user: ApiPostUser; + export let api: ReturnType<typeof trpc>; + let sOk = api.apiAuth.isApiSessionValid.createQuery( + { checkingUserSession: true, userId: user.id }, + { refetchOnWindowFocus: false, refetchInterval: false, retry: 0 }, + ); + $: isLoading = $sOk.isLoading || $sOk.isFetching; + $: isApiSessionValid = !isLoading && $sOk.data?.valid === true; + + let logoutUserM = api.apiAuth.logoutUser.createMutation({ + onSuccess: () => { + $sOk.refetch(); + }, + }); + + const onSuccessfulLogin = () => { + $sOk.refetch(); + }; + + function logoutUser() { + $logoutUserM.mutateAsync({ userId: user.id }); + } + function checkSession() { + $sOk.refetch(); + } +</script> + +<div class="flex items-center justify-between gap-2"> + {#if isLoading} + <Pill text={"Verifying..."} theme={"slate"} /> + {:else if isApiSessionValid} + <div class="flex flex-col gap-2 items-center"> + <div + class="flex items-center gap-2 px-4 p-2 rounded-full bg-emerald-50 text-emerald-500 w-max justify-center" + > + Active + <CheckIcon class="w-6 h-auto" /> + </div> + <div class="items-center flex flex-row gap-2 w-full"> + <div class="flex items-center gap-2"> + <Button + variant="primaryInverted" + on:click={() => checkSession()} + > + Check + </Button> + + <Button + variant="secondary" + on:click={() => logoutUser()} + class="flex items-center gap-2" + > + Logout + <LogOutIcon class="w-4 h-auto" /> + </Button> + </div> + </div> + </div> + {:else if !isLoading && !isApiSessionValid} + <SetNewSessionForm {user} {api} {onSuccessfulLogin} /> + {:else} + <Pill text={"invalid"} theme={"rose"} /> + {/if} +</div> diff --git a/src/routes/admin/post-data-config/set-new-session-form.svelte b/src/routes/admin/post-data-config/set-new-session-form.svelte new file mode 100755 index 0000000..7b058f3 --- /dev/null +++ b/src/routes/admin/post-data-config/set-new-session-form.svelte @@ -0,0 +1,110 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import type { TRPC } from "$lib/trpc/trpc"; + import type { ApiPostUser } from "$lib/utils/data.types"; + import toast from "svelte-french-toast"; + import { onMount } from "svelte"; + + export let api: TRPC; + export let user: ApiPostUser; + export let onSuccessfulLogin: () => void; + + let signingIn = false; + let captchaAnswer = ""; + + let getNewSessionM = api.apiAuth.getNewSession.createMutation({ + onSuccess: (o) => { + signingIn = false; + if (!o.success) { + for (const each of o.errors) { + toast.error(each.message); + } + return; + } + toast.success("Successfully authenticated", { + duration: 1000 * 10, + }); + onSuccessfulLogin(); + }, + onError: (e) => { + console.log(e); + signingIn = false; + toast.error( + "An error occured while authenticating, please try again later", + ); + }, + }); + + let captchaImage = ""; + let captchaId = ""; + + let captchaQ = api.apiAuth.getCaptcha.createMutation({ + onSuccess: (d) => { + captchaId = d.id; + captchaImage = d.image; + }, + onError: (e) => { + toast.error( + "An error occured while loading captcha, please try again later", + ); + }, + }); + + $: fetchingCaptcha = $captchaQ.isLoading; + + const onFormSubmit = async () => { + if (captchaAnswer.length !== 4) { + toast.error("Captcha answer must be 4 characters long"); + return; + } + if ($captchaQ.data?.id) { + signingIn = true; + $getNewSessionM.mutateAsync({ + captchaId: $captchaQ.data?.id, + captchaAnswer, + userId: user.id, + }); + } else { + toast.error("Captcha not loaded, refresh and try again"); + } + }; + + onMount(() => { + $captchaQ.mutateAsync(); + }); +</script> + +<form + class="flex flex-col gap-4 w-full" + on:submit|preventDefault={onFormSubmit} +> + {#if !fetchingCaptcha} + <img + class="w-64 h-16 rounded-md" + src={`data:image/jpeg;base64,${captchaImage}`} + alt={"Captcha"} + /> + {:else} + <div class="w-64 h-16 rounded-md bg-gray-200 animate-pulse"></div> + {/if} + <div class="gap-2 flex items-center"> + <input hidden name="captchaId" value={captchaId} /> + <Input + bind:value={captchaAnswer} + name={"captchaAnswer"} + placeholder={"Captcha Answer"} + otherInputOptions={{ + required: true, + minLength: 4, + maxLength: 4, + }} + /> + <Button + disabled={signingIn} + text={signingIn ? "Logging in..." : "Login"} + intent={signingIn ? "ghost" : "primary"} + otherOptions={{ type: "submit" }} + /> + </div> +</form> diff --git a/src/routes/admin/post-data-config/stores.ts b/src/routes/admin/post-data-config/stores.ts new file mode 100644 index 0000000..bed5201 --- /dev/null +++ b/src/routes/admin/post-data-config/stores.ts @@ -0,0 +1,43 @@ +import type { + PostDataEntry, + PostDataHistoryFilters, +} from "$lib/utils/data.types"; +import { writable } from "svelte/store"; +import { + type TableOptions, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, +} from "@tanstack/svelte-table"; + +export const filters = writable<PostDataHistoryFilters>({ + date: new Date().toISOString().split("T")[0], + draw: { + id: "", + title: "", + closeTime: "", + filterDuplicatesWhilePosting: false, + drawType: 0, + adminId: 0, + abRateF: 0, + abRateS: 0, + abcRateF: 0, + abcRateS: 0, + }, +}); + +export const postDataTableOptions = writable<TableOptions<PostDataEntry>>({ + data: [], + columns: [], + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableGlobalFilter: true, + // globalFilterFn: fuzzyFilter, +}); + +export const setPostDataTableData = (data: PostDataEntry[]) => { + postDataTableOptions.update((state) => ({ ...state, data: data.reverse() })); +}; diff --git a/src/routes/admin/post-data-history/+page.svelte b/src/routes/admin/post-data-history/+page.svelte new file mode 100644 index 0000000..4f9c617 --- /dev/null +++ b/src/routes/admin/post-data-history/+page.svelte @@ -0,0 +1,177 @@ +<script lang="ts"> + import { trpc } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import type { ApiPostUser, PostDataEntry } from "$lib/utils/data.types"; + import { setPostDataTableData, postDataTableOptions } from "./stores"; + import Title from "$lib/components/atoms/title.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + import PostDataView from "./post-data-view.svelte"; + import { Button } from "$lib/components/ui/button"; + import CenteredSpinner from "$lib/components/molecules/centered-spinner.svelte"; + import DataFetchingFilters from "./data-fetching-filters.svelte"; + import { filters } from "./stores"; + import Pill from "$lib/components/atoms/pill.svelte"; + + const api = trpc(); + + let showFilters = false; + let affectingRowVisibility = false; + let chosenUser: ApiPostUser | undefined = undefined; + let usersSelect = [] as { label: string; value: string }[]; + let postdata = { + data: [] as PostDataEntry[], + users: [] as ApiPostUser[], + posted: false, + }; + let chosenData = [] as PostDataEntry[]; + + let drawsQ = api.draw.getAllDraws.createQuery(undefined, { + refetchOnWindowFocus: false, + retry: 3, + }); + $: drawsLoading = $drawsQ.isLoading || $drawsQ.isFetching; + + function setChosenUser(e: any) { + affectingRowVisibility = true; + const chosen = e.target.value; + chosenUser = postdata.users.find((u) => u.userName === chosen); + setChosenData(); + setTimeout(() => { + affectingRowVisibility = false; + }, 200); + } + + function setChosenData() { + console.log(chosenUser, postdata.data.slice(0, 10)); + chosenData = postdata.data.filter( + (d) => d.userId === chosenUser?.userId, + ); + setPostDataTableData(chosenData); + } + + let fetchPostDataHistoryM = + api.postData.fetchPostDataHistory.createMutation({ + onSuccess: (d) => { + console.log(d); + if (d.data) { + postdata.data = d.data; + postdata.users = d.users; + usersSelect = d.users.map((u) => ({ + label: `${u.userName} - ${u.userId}`, + value: u.userName, + })); + chosenUser = d.users[0]; + } else if (!d.ok) { + toast.error(d.detail); + postdata = { data: [], users: [], posted: false }; + return; + } + toast("Data fetched successfully."); + setChosenData(); + }, + onError: (e) => { + console.log(e); + toast( + "An error occurred while fetching data. Try again after a page refresh.", + ); + }, + }); + + $: overallTotals = { + f: postdata.data.reduce((a, b) => a + b.first, 0), + s: postdata.data.reduce((a, b) => a + b.second, 0), + }; + + $: totals = { + f: chosenData.reduce((a, b) => a + b.first, 0), + s: chosenData.reduce((a, b) => a + b.second, 0), + }; + + const fetchHistory = async () => { + $fetchPostDataHistoryM.mutateAsync({ + date: $filters.date, + draw: $filters.draw!, + }); + }; +</script> + +<section class="w-full grid place-items-center p-8"> + <div + class="w-full flex flex-col gap-4 max-w-8xl p-6 lg:p-8 rounded-lg shadow-sm border" + > + <div + class="flex flex-col md:flex-row gap-4 justify-between w-full items-center" + > + <Title text={"Posted Data History"} size={"h1"} /> + <Button + variant={"primaryInverted"} + disabled={drawsLoading} + on:click={() => { + showFilters = !showFilters; + }} + > + Toggle filters + </Button> + </div> + + {#if drawsLoading} + <CenteredSpinner /> + {:else} + <DataFetchingFilters + bind:showFilters + draws={$drawsQ.data} + fetchDataFn={fetchHistory} + /> + {/if} + + {#if drawsLoading} + <CenteredSpinner /> + {:else} + <div class="flex gap-4 items-center flex-col md:flex-row"> + {#if chosenData.length > 0 && !$fetchPostDataHistoryM.isLoading} + <div class="w-full sm:max-w-64"> + <Select + fullWidth + label={"User's Data"} + options={usersSelect} + onSelect={setChosenUser} + /> + </div> + {/if} + </div> + + {#if $fetchPostDataHistoryM.isLoading} + <CenteredSpinner /> + {:else if chosenData.length > 0 && !affectingRowVisibility} + <PostDataView rowCount={chosenData.length} {chosenUser} /> + + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Overall totals :"} theme={"sky"} /> + <Pill + text={`F rate : ${overallTotals.f}`} + theme={"slate"} + /> + <Pill + text={`S rate : ${overallTotals.s}`} + theme={"slate"} + /> + </div> + + <div class="flex w-full gap-2 flex-wrap"> + <Pill + text={`Totals for ${chosenUser?.userName} :`} + theme={"sky"} + /> + <Pill text={`F rate : ${totals.f}`} theme={"slate"} /> + <Pill text={`S rate : ${totals.s}`} theme={"slate"} /> + </div> + {:else} + <div class="flex justify-center items-center h-64"> + <p class="text-gray-500 text-lg"> + No history found for the selected date and draw. + </p> + </div> + {/if} + {/if} + </div> +</section> diff --git a/src/routes/admin/post-data-history/data-fetch-config.svelte b/src/routes/admin/post-data-history/data-fetch-config.svelte new file mode 100644 index 0000000..d3ec5b0 --- /dev/null +++ b/src/routes/admin/post-data-history/data-fetch-config.svelte @@ -0,0 +1,104 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import { Input } from "$lib/components/ui/input"; + import { Label } from "$lib/components/ui/label"; + import Title from "$lib/components/atoms/title.svelte"; + import { + zPostDataHistoryFilters, + type PostDataHistoryFilters, + type Draw, + } from "$lib/utils/data.types"; + import toast from "svelte-french-toast"; + import Switch from "$lib/components/atoms/switch.svelte"; + import { filters } from "./stores"; + + export let fetchDataFn: (filters: PostDataHistoryFilters) => Promise<void>; + export let updateDrawFilter: (draws: Draw[]) => void; + export let showFilters: boolean; + export let draws: Draw[] = []; + export let fetching: boolean = false; + + function saveDrawFilterChanges() { + console.log(draws); + updateDrawFilter(draws); + } + + function fetchOrGenerate() { + const _filters = { + date: $filters.date, + draw: $filters.draw, + } as PostDataHistoryFilters; + const parsed = zPostDataHistoryFilters.safeParse(_filters); + if (!parsed.success) { + toast.error("Please fill all the fields properly."); + return; + } + showFilters = false; + fetchDataFn(parsed.data); + } + + onMount(() => { + filters.update((d) => ({ ...d, draw: draws[0] })); + }); +</script> + +<div class="flex flex-col w-full gap-8 rounded-lg shadow-sm border p-8"> + <Title text="Draw Presets" size="h2" /> + + <div class="w-full flex flex-col gap-4 lg:grid xs:grid-cols-2"> + {#each draws as draw} + <div class="w-full flex flex-col gap-4"> + <Title text={draw.title} size="h4" /> + + <div class="flex gap-4 w-full flex-col md:flex-row"> + <div class="w-full space-y-2"> + <Label>2 Digit rate (F)</Label> + <Input + bind:value={draw.abRateF} + type={"number"} + min={0} + /> + </div> + <div class="w-full space-y-2"> + <Label>2 Digit rate (S)</Label> + <Input + bind:value={draw.abRateS} + type={"number"} + min={0} + /> + </div> + </div> + + <div class="flex gap-4 w-full flex-col md:flex-row"> + <div class="w-full space-y-2"> + <Label>3 Digit rate (F)</Label> + <Input + bind:value={draw.abcRateF} + type={"number"} + min={0} + /> + </div> + <div class="w-full space-y-2"> + <Label>3 Digit rate (S)</Label> + <Input + bind:value={draw.abcRateS} + type={"number"} + min={0} + /> + </div> + </div> + + <Switch + label={"Filter numbers with repeating digits"} + bind:checked={draw.filterDuplicatesWhilePosting} + /> + <div class="w-full my-4"></div> + </div> + {/each} + </div> + <Button + text={"Save changes"} + disabled={fetching} + onClick={() => saveDrawFilterChanges()} + /> +</div> diff --git a/src/routes/admin/post-data-history/data-fetching-filters.svelte b/src/routes/admin/post-data-history/data-fetching-filters.svelte new file mode 100755 index 0000000..650a790 --- /dev/null +++ b/src/routes/admin/post-data-history/data-fetching-filters.svelte @@ -0,0 +1,100 @@ +<script lang="ts"> + import { Button } from "$lib/components/ui/button"; + import Input from "$lib/components/atoms/input.svelte"; + import Line from "$lib/components/atoms/line.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + import type { Draw } from "$lib/utils/data.types"; + import toast from "svelte-french-toast"; + import { onMount } from "svelte"; + import { filters } from "./stores"; + + export let fetchDataFn: () => Promise<void>; + export let showFilters: boolean; + export let draws: Draw[] = []; + export let fetchingData: boolean = false; + + $: drawsCount = draws.length; + onMount(() => { + filters.update((f) => ({ ...f, draw: draws[0] })); + }); +</script> + +{#if drawsCount > 0 && showFilters} + <section class="flex flex-col md:flex-row gap-4 w-full"> + <Input + bind:value={$filters.date} + label={"Date"} + inputType={"date"} + placeholder={"Date"} + /> + <Select + defaultChosen={$filters.draw?.id} + fullWidth + label={"Draw"} + options={draws.map((e) => { + return { label: e.title, value: e.id, id: e.id }; + })} + onSelect={(e) => { + if (drawsCount > 0) { + filters.update((f) => ({ + ...f, + // @ts-ignore + draw: draws.find((d) => d.id === e.target.value), + })); + } + }} + /> + </section> + <div class="flex justify-end w-full"> + <Button + disabled={fetchingData} + on:click={async () => { + if ( + $filters.date.length < 1 || + !$filters.draw || + !$filters.draw.id + ) { + toast.error("Select all filters properly to fetch data"); + return; + } + showFilters = false; + toast("Fetching data for selected filters"); + await fetchDataFn(); + }} + > + Apply filters + </Button> + </div> +{:else if !showFilters} + <div + class="flex flex-col md:flex-row gap-4 items-center justify-between w-full" + > + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Data Filters:"} theme={"sky"} /> + <Pill text={$filters.date} theme={"slate"} /> + <Pill text={$filters.draw?.title} theme={"slate"} /> + </div> + + <Button + variant={fetchingData ? "ghost" : "default"} + disabled={fetchingData} + on:click={async () => { + if ( + $filters.date.length < 1 || + !$filters.draw || + !$filters.draw.id + ) { + toast.error("Select all filters properly to fetch data"); + return; + } + showFilters = false; + toast("Fetching data for selected filters"); + await fetchDataFn(); + }} + > + {fetchingData ? "Fetching" : "Fetch data"} + </Button> + </div> +{/if} +<Line /> diff --git a/src/routes/admin/post-data-history/post-data-view.svelte b/src/routes/admin/post-data-history/post-data-view.svelte new file mode 100644 index 0000000..c46a95f --- /dev/null +++ b/src/routes/admin/post-data-history/post-data-view.svelte @@ -0,0 +1,233 @@ +<script lang="ts"> + import type { ApiPostUser, PostDataEntry } from "$lib/utils/data.types"; + import { + createColumnHelper, + createSvelteTable, + flexRender, + type SortDirection, + } from "@tanstack/svelte-table"; + import { postDataTableOptions } from "./stores"; + import Select from "$lib/components/atoms/select.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import { clsx } from "clsx"; + import Pagination from "$lib/components/atoms/pagination.svelte"; + + export let chosenUser: ApiPostUser | undefined = undefined; + + const minPageSize = 100; + + export let rowCount: number; + let updatingPaginationState = false; + let pagination = { pageSize: minPageSize, pageIndex: 0 }; + + const getSortingSymbol = (isSorted: boolean | SortDirection) => { + return isSorted === "asc" ? "▲" : isSorted === "desc" ? "▼" : ""; + }; + + const setCurrentPage = (page: number) => { + postDataTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageIndex: page, + pageSize: state.state?.pagination?.pageSize ?? minPageSize, + }, + }, + })); + }; + const setPaginationPageSize = (size: number) => { + updatingPaginationState = true; + setCurrentPage(0); + postDataTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageSize: size, + pageIndex: state.state?.pagination?.pageIndex ?? 0, + }, + }, + })); + setTimeout(() => { + updatingPaginationState = false; + }, 100); + }; + const colHelper = createColumnHelper<PostDataEntry>(); + const columns = [ + colHelper.display({ + id: "number", + cell: (info) => info.row.original.number, + header: "No", + // @ts-ignore + accessorFn: (r) => r.number.toString(), + }), + colHelper.display({ + id: "first", + cell: (info) => info.row.original.first, + header: "First", + // @ts-ignore + accessorFn: (r) => r.first.toString(), + }), + colHelper.display({ + id: "second", + cell: (info) => info.row.original.second, + header: "Second", + // @ts-ignore + accessorFn: (r) => r.second.toString(), + }), + colHelper.display({ + id: "user", + cell: (info) => chosenUser?.userName ?? "", + header: "User", + // @ts-ignore + accessorFn: (r) => chosenUser?.userName ?? "", + }), + ]; + + let searchTimeOut: NodeJS.Timeout; + const handleEntrySearch = (e: any) => { + clearTimeout(searchTimeOut); + searchTimeOut = setTimeout(() => { + postDataTableOptions.update((opts: any) => { + return { + ...opts, + state: { + ...opts.state, + columnFilters: [ + { id: "number", value: e.target.value }, + ], + }, + }; + }); + }, 300); + }; + + const typeAnnotateComponent = (a: any) => { + return a as any; + }; + + const table = createSvelteTable(postDataTableOptions); + + onMount(() => { + postDataTableOptions.update((p) => { + return { + ...p, + columns, + state: { ...p.state, pagination }, + }; + }); + setPaginationPageSize(minPageSize); + }); +</script> + +<!-- INFO: The table itself --> +<div class="w-full text-black"> + <div class="my-2 flex w-full justify-between items-center gap-4"> + <div class="flex justify-end md:max-w-xs w-full"> + <Input + label={"Search"} + placeholder={"Lookup numbers"} + fieldWidth={"full"} + onInput={handleEntrySearch} + /> + </div> + <div + class="flex justify-between flex-col w-full md:flex-row md:max-w-xs gap-2" + > + <Select + fullWidth + label={"Row count"} + options={[100, 250, 500, 1000].map((n) => { + return { + id: n.toString(), + label: n.toString(), + value: n.toString(), + }; + })} + onSelect={(e) => { + // @ts-ignore + const chosen = Number(e.target.value); + setPaginationPageSize(chosen); + }} + /> + </div> + </div> + + <div class="w-full overflow-y-auto h-[69vh]"> + <table class="w-full"> + <thead> + {#each $table.getHeaderGroups() as headerGroup} + <tr> + {#each headerGroup.headers as header} + <th + class="p-2 px-4 border sticky top-0 bg-sky-50 text-sky-700" + colSpan={header.colSpan} + > + {#if !header.isPlaceholder} + <button + class={clsx( + "flex outline-none justify-center items-center gap-1 w-full h-full", + )} + disabled={!header.column.getCanSort()} + on:click={header.column.getToggleSortingHandler()} + > + <svelte:component + this={typeAnnotateComponent( + flexRender( + header.column.columnDef + .header, + header.getContext(), + ), + )} + /> + <span + >{getSortingSymbol( + header.column.getIsSorted(), + )}</span + > + </button> + {/if} + </th> + {/each} + </tr> + {/each} + </thead> + <tbody> + {#each $table.getRowModel().rows as row} + <tr + class="cursor-pointer hover:bg-slate-50 transition-colors duration-100" + on:click={() => { + // fetchFSRow(row.original.number); + }} + > + {#each row.getVisibleCells() as cell} + <td class="p-1 px-4 border text-center"> + <svelte:component + this={typeAnnotateComponent( + flexRender( + cell.column.columnDef.cell, + cell.getContext(), + ), + )} + /> + </td> + {/each} + </tr> + {/each} + </tbody> + </table> + </div> + + {#if !updatingPaginationState} + <div class="w-full py-2"> + <Pagination + count={rowCount} + page={$table.getState().pagination.pageIndex + 1} + perPage={$table.getState().pagination.pageSize} + siblingCount={1} + setPage={setCurrentPage} + /> + </div> + {/if} +</div> diff --git a/src/routes/admin/post-data-history/stores.ts b/src/routes/admin/post-data-history/stores.ts new file mode 100644 index 0000000..d91e402 --- /dev/null +++ b/src/routes/admin/post-data-history/stores.ts @@ -0,0 +1,29 @@ +import type { Draw, PostDataEntry } from "$lib/utils/data.types"; +import { writable } from "svelte/store"; +import { + type TableOptions, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, +} from "@tanstack/svelte-table"; + +export const filters = writable<{ date: string; draw: Draw | undefined }>({ + date: new Date().toISOString().split("T")[0], + draw: undefined, +}); + +export const postDataTableOptions = writable<TableOptions<PostDataEntry>>({ + data: [], + columns: [], + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableGlobalFilter: true, + // globalFilterFn: fuzzyFilter, +}); + +export const setPostDataTableData = (data: PostDataEntry[]) => { + postDataTableOptions.update((state) => ({ ...state, data })); +}; diff --git a/src/routes/admin/post-data-panel/+page.svelte b/src/routes/admin/post-data-panel/+page.svelte new file mode 100644 index 0000000..1079a02 --- /dev/null +++ b/src/routes/admin/post-data-panel/+page.svelte @@ -0,0 +1,125 @@ +<script lang="ts"> + import { trpc } from "$lib/trpc/trpc"; + import toast from "svelte-french-toast"; + import Title from "$lib/components/atoms/title.svelte"; + import PresetDataView from "./preset-data-view.svelte"; + import CenteredSpinner from "$lib/components/molecules/centered-spinner.svelte"; + import DataFetchingFilters from "./data-fetching-filters.svelte"; + import { + checkMap, + filters, + setPresetDataTableData, + presetDataTableOptions, + } from "./stores"; + import DataEntryInputSection from "./data-entry-input-section.svelte"; + import { onMount } from "svelte"; + import { hasDrawBeenClosed } from "$lib/utils"; + import { nowDT } from "../stores"; + + const api = trpc(); + + let affectingRowVisibility = false; + + let drawsQ = api.draw.getAllDraws.createQuery(undefined, { + refetchOnWindowFocus: false, + retry: 3, + }); + $: drawsLoading = $drawsQ.isLoading || $drawsQ.isFetching; + + let fetchPresetDataM = api.presetData.getAll.createMutation({ + onSuccess: (out) => { + affectingRowVisibility = true; + console.log(out); + if (out.data && out.ok) { + setPresetDataTableData(out.data); + } else { + toast.error(out.detail); + } + setTimeout(() => { + affectingRowVisibility = false; + }, 200); + }, + onError: (e) => { + console.log(e); + toast( + "An error occurred while fetching data. Try again after a page refresh.", + ); + }, + }); + + let deletePresetDataM = api.presetData.delete.createMutation({ + onSuccess: (d) => { + console.log(d); + toast.success("Data deleted successfully"); + $fetchPresetDataM.mutateAsync({ + date: $filters.date, + draw: $filters.draw, + }); + }, + onError: (e) => { + console.log(e); + toast.error("An error occurred while deleting data"); + }, + }); + + function deleteSelectedEntries() { + const ids = [] as string[]; + + for (const each in $checkMap) { + if (!$checkMap[each]) continue; + const found = $presetDataTableOptions.data.find( + (e) => e.id === each, + ); + if (found) { + ids.push(found.id); + } + } + $deletePresetDataM.mutateAsync({ date: $filters.date, ids }); + } + + let hasTimePassed = false; + + onMount(() => { + filters.subscribe((val) => { + if (val.date && val.draw) { + $fetchPresetDataM.mutateAsync({ + date: val.date, + draw: val.draw, + }); + } + + hasTimePassed = hasDrawBeenClosed(val.date, val.draw, $nowDT); + }); + }); +</script> + +<section class="w-full grid place-items-center p-8"> + <div + class="w-full flex flex-col gap-4 max-w-6xl p-6 lg:p-8 rounded-lg shadow-sm border" + > + <div + class="flex flex-col md:flex-row gap-4 justify-between w-full items-center" + > + <Title text={"Preset Data"} size={"h1"} /> + </div> + + {#if !drawsLoading} + <DataFetchingFilters draws={$drawsQ.data} /> + {/if} + + {#if drawsLoading || $fetchPresetDataM.isLoading} + <CenteredSpinner /> + {:else if !affectingRowVisibility} + <PresetDataView {deleteSelectedEntries} /> + {#if !hasTimePassed} + <DataEntryInputSection {api} /> + {/if} + {:else} + <div class="flex justify-center items-center h-64"> + <p class="text-gray-500 text-lg"> + No preset data found for the chosen date, draw and user + </p> + </div> + {/if} + </div> +</section> diff --git a/src/routes/admin/post-data-panel/data-entry-input-section.svelte b/src/routes/admin/post-data-panel/data-entry-input-section.svelte new file mode 100644 index 0000000..547ed33 --- /dev/null +++ b/src/routes/admin/post-data-panel/data-entry-input-section.svelte @@ -0,0 +1,73 @@ +<script lang="ts"> + import { Button } from "$lib/components/ui/button"; + import { Textarea } from "$lib/components/ui/textarea"; + import type { TRPC } from "$lib/trpc/trpc"; + import type { PresetDataEntry } from "$lib/utils/data.types"; + import { parseEntriesFromMessage } from "$lib/utils/entry.parser"; + import toast from "svelte-french-toast"; + import { filters, setPresetDataTableData } from "./stores"; + import Switch from "$lib/components/atoms/switch.svelte"; + import { Input } from "$lib/components/ui/input"; + + export let api: TRPC; + let postingData = [] as PresetDataEntry[]; + let isPosting = false; + let isSingleEntry = false; + + const insertDataM = api.presetData.insert.createMutation({ + onSuccess: (d) => { + console.log(d); + if (!d.ok) { + return toast.error(d.detail); + } + setPresetDataTableData(d.data ?? [], true); + message = ""; + isPosting = false; + }, + onError: (e) => { + console.log(e); + }, + }); + + let message = ""; + + function handleSubmit() { + const parsed = parseEntriesFromMessage(message); + if (message.length < 1 || parsed.length < 1) { + return toast.error("Please enter some data to upload."); + } + postingData = parsed.map((r) => ({ + ...r, + drawId: +$filters.draw!.id.split(":")[1], + bookDate: $filters.date, + })); + isPosting = true; + $insertDataM.mutateAsync(postingData); + } +</script> + +<!-- <Switch bind:checked={isSingleEntry} label={"Single entry mode"} /> --> + +<!-- {#if isSingleEntry} + <div class="flex items-center gap-4 justify-center"> + <Input placeholder={"No"} class="max-w-32" /> + <Input placeholder={"F"} class="max-w-32" /> + <Input placeholder={"S"} class="max-w-32" /> + </div> +{:else} --> +<Textarea + name="customData" + bind:value={message} + on:input={(e) => { + // @ts-ignore + message = e.target.value.replace(/[^0-9+.fs\n]/g, ""); + }} +></Textarea> +<Button + disabled={isPosting} + variant={"primaryInverted"} + on:click={handleSubmit} +> + {isPosting ? "Submitting..." : "Submit Data"} +</Button> +<!-- {/if} --> diff --git a/src/routes/admin/post-data-panel/data-fetch-config.svelte b/src/routes/admin/post-data-panel/data-fetch-config.svelte new file mode 100644 index 0000000..d3ec5b0 --- /dev/null +++ b/src/routes/admin/post-data-panel/data-fetch-config.svelte @@ -0,0 +1,104 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import { Input } from "$lib/components/ui/input"; + import { Label } from "$lib/components/ui/label"; + import Title from "$lib/components/atoms/title.svelte"; + import { + zPostDataHistoryFilters, + type PostDataHistoryFilters, + type Draw, + } from "$lib/utils/data.types"; + import toast from "svelte-french-toast"; + import Switch from "$lib/components/atoms/switch.svelte"; + import { filters } from "./stores"; + + export let fetchDataFn: (filters: PostDataHistoryFilters) => Promise<void>; + export let updateDrawFilter: (draws: Draw[]) => void; + export let showFilters: boolean; + export let draws: Draw[] = []; + export let fetching: boolean = false; + + function saveDrawFilterChanges() { + console.log(draws); + updateDrawFilter(draws); + } + + function fetchOrGenerate() { + const _filters = { + date: $filters.date, + draw: $filters.draw, + } as PostDataHistoryFilters; + const parsed = zPostDataHistoryFilters.safeParse(_filters); + if (!parsed.success) { + toast.error("Please fill all the fields properly."); + return; + } + showFilters = false; + fetchDataFn(parsed.data); + } + + onMount(() => { + filters.update((d) => ({ ...d, draw: draws[0] })); + }); +</script> + +<div class="flex flex-col w-full gap-8 rounded-lg shadow-sm border p-8"> + <Title text="Draw Presets" size="h2" /> + + <div class="w-full flex flex-col gap-4 lg:grid xs:grid-cols-2"> + {#each draws as draw} + <div class="w-full flex flex-col gap-4"> + <Title text={draw.title} size="h4" /> + + <div class="flex gap-4 w-full flex-col md:flex-row"> + <div class="w-full space-y-2"> + <Label>2 Digit rate (F)</Label> + <Input + bind:value={draw.abRateF} + type={"number"} + min={0} + /> + </div> + <div class="w-full space-y-2"> + <Label>2 Digit rate (S)</Label> + <Input + bind:value={draw.abRateS} + type={"number"} + min={0} + /> + </div> + </div> + + <div class="flex gap-4 w-full flex-col md:flex-row"> + <div class="w-full space-y-2"> + <Label>3 Digit rate (F)</Label> + <Input + bind:value={draw.abcRateF} + type={"number"} + min={0} + /> + </div> + <div class="w-full space-y-2"> + <Label>3 Digit rate (S)</Label> + <Input + bind:value={draw.abcRateS} + type={"number"} + min={0} + /> + </div> + </div> + + <Switch + label={"Filter numbers with repeating digits"} + bind:checked={draw.filterDuplicatesWhilePosting} + /> + <div class="w-full my-4"></div> + </div> + {/each} + </div> + <Button + text={"Save changes"} + disabled={fetching} + onClick={() => saveDrawFilterChanges()} + /> +</div> diff --git a/src/routes/admin/post-data-panel/data-fetching-filters.svelte b/src/routes/admin/post-data-panel/data-fetching-filters.svelte new file mode 100755 index 0000000..fb56b9b --- /dev/null +++ b/src/routes/admin/post-data-panel/data-fetching-filters.svelte @@ -0,0 +1,39 @@ +<script lang="ts"> + import Input from "$lib/components/atoms/input.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + import type { Draw } from "$lib/utils/data.types"; + import { onMount } from "svelte"; + import { filters } from "./stores"; + + export let draws: Draw[] = []; + + onMount(() => { + filters.update((f) => ({ ...f, draw: draws[0] })); + }); +</script> + +<section class="flex flex-col md:flex-row gap-4 w-full"> + <Input + bind:value={$filters.date} + label={"Date"} + inputType={"date"} + placeholder={"Date"} + /> + <Select + defaultChosen={$filters.draw?.id} + fullWidth + label={"Draw"} + options={draws.map((e) => { + return { label: e.title, value: e.id, id: e.id }; + })} + onSelect={(e) => { + if (draws.length > 0) { + filters.update((f) => ({ + ...f, + // @ts-ignore + draw: draws.find((d) => d.id === e.target.value), + })); + } + }} + /> +</section> diff --git a/src/routes/admin/post-data-panel/preset-data-view.svelte b/src/routes/admin/post-data-panel/preset-data-view.svelte new file mode 100644 index 0000000..7b6fcf9 --- /dev/null +++ b/src/routes/admin/post-data-panel/preset-data-view.svelte @@ -0,0 +1,302 @@ +<script lang="ts"> + import type { PresetDataEntry } from "$lib/utils/data.types"; + import { + createColumnHelper, + createSvelteTable, + flexRender, + type SortDirection, + } from "@tanstack/svelte-table"; + import { presetDataTableOptions } from "./stores"; + import Select from "$lib/components/atoms/select.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import { clsx } from "clsx"; + import Pagination from "$lib/components/atoms/pagination.svelte"; + import Checkbox from "$lib/components/ui/checkbox/checkbox.svelte"; + import { checkMap } from "./stores"; + import { Button } from "$lib/components/ui/button"; + import * as AlertDialog from "$lib/components/ui/alert-dialog/index"; + + const minPageSize = 100; + + export let rowCount: number = 0; + export let deleteSelectedEntries: () => void; + let updatingPaginationState = false; + let pagination = { pageSize: minPageSize, pageIndex: 0 }; + let globalFilter = ""; + + const getSortingSymbol = (isSorted: boolean | SortDirection) => { + return isSorted === "asc" ? "▲" : isSorted === "desc" ? "▼" : ""; + }; + + const setCurrentPage = (page: number) => { + presetDataTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageIndex: page, + pageSize: state.state?.pagination?.pageSize ?? minPageSize, + }, + }, + })); + }; + + const setPaginationPageSize = (size: number) => { + updatingPaginationState = true; + setCurrentPage(0); + presetDataTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageSize: size, + pageIndex: state.state?.pagination?.pageIndex ?? 0, + }, + }, + })); + setTimeout(() => { + updatingPaginationState = false; + }, 100); + }; + + const setGlobalFilter = (filter: string) => { + setPaginationPageSize($table.getState().pagination.pageSize); + globalFilter = filter; + presetDataTableOptions.update((p) => { + return { ...p, state: { ...p.state, globalFilter: filter } }; + }); + rowCount = $table.getFilteredRowModel().rows.length; + }; + const colHelper = createColumnHelper<PresetDataEntry>(); + const columns = [ + colHelper.display({ + id: "number", + cell: (info) => info.row.original.number, + header: "No", + // @ts-ignore + accessorFn: (r) => r.number.toString(), + }), + colHelper.display({ + id: "first", + cell: (info) => info.row.original.first, + header: "First", + // @ts-ignore + accessorFn: (r) => r.first.toString(), + }), + colHelper.display({ + id: "second", + cell: (info) => info.row.original.second, + header: "Second", + // @ts-ignore + accessorFn: (r) => r.second.toString(), + }), + ]; + + let searchTimeOut: NodeJS.Timeout; + const handleEntrySearch = (e: any) => { + clearTimeout(searchTimeOut); + searchTimeOut = setTimeout(() => { + setGlobalFilter(e.target.value); + }, 300); + }; + + const typeAnnotateComponent = (a: any) => { + return a as any; + }; + + const table = createSvelteTable(presetDataTableOptions); + + onMount(() => { + presetDataTableOptions.update((p) => { + return { + ...p, + columns, + state: { ...p.state, globalFilter, pagination }, + }; + }); + setPaginationPageSize(minPageSize); + presetDataTableOptions.subscribe((opts) => { + rowCount = opts.data.length; + }); + }); + + $: chosenForDeletion = Object.values($checkMap).filter((v) => v).length; +</script> + +<!-- INFO: The table itself --> +<div class="w-full text-black"> + <div class="my-2 flex w-full justify-between items-center gap-4"> + <div class="flex items-end gap-2"> + <div class="flex justify-end md:max-w-xs w-full"> + <Input + label={"Search"} + placeholder={"Type anything"} + fieldWidth={"full"} + onInput={handleEntrySearch} + /> + </div> + + {#if chosenForDeletion > 0} + <AlertDialog.Root> + <AlertDialog.Trigger> + <Button + disabled={chosenForDeletion === 0} + variant="destructiveInverted" + > + Delete Selected ({chosenForDeletion}) + </Button> + </AlertDialog.Trigger> + <AlertDialog.Content> + <AlertDialog.Header> + <AlertDialog.Title> + Deleting {chosenForDeletion} entries + </AlertDialog.Title> + <AlertDialog.Description> + This action cannot be undone. This will + permanently delete the selected entries. + </AlertDialog.Description> + </AlertDialog.Header> + <AlertDialog.Footer> + <AlertDialog.Cancel + >Cancel & Go back</AlertDialog.Cancel + > + <AlertDialog.Action + on:click={() => { + deleteSelectedEntries(); + }} + > + Confirm & Delete + </AlertDialog.Action> + </AlertDialog.Footer> + </AlertDialog.Content> + </AlertDialog.Root> + {/if} + </div> + <div + class="flex justify-between flex-col w-full md:flex-row md:max-w-xs gap-2" + > + <Select + fullWidth + label={"Row count"} + options={[100, 250, 500, 1000].map((n) => { + return { + id: n.toString(), + label: n.toString(), + value: n.toString(), + }; + })} + onSelect={(e) => { + // @ts-ignore + const chosen = Number(e.target.value); + setPaginationPageSize(chosen); + }} + /> + </div> + </div> + + <div class="w-full overflow-y-auto h-[69vh] border"> + <table class="w-full"> + <thead> + {#each $table.getHeaderGroups() as headerGroup} + <tr> + <th + class="p-2 px-4 border sticky top-0 bg-sky-50 text-sky-700 w-20" + > + <div> + <Checkbox + checked={Object.values($checkMap).reduce( + (acc, curr) => acc && curr, + true, + )} + onCheckedChange={(checked) => { + console.log(checked); + checkMap.update((ob) => { + const nO = {}; + for (const key in ob) { + // @ts-ignore + nO[key] = checked; + } + return nO; + }); + }} + /> + </div> + </th> + {#each headerGroup.headers as header} + <th + class="p-2 px-4 border sticky top-0 bg-sky-50 text-sky-700" + colspan={header.colSpan} + > + {#if !header.isPlaceholder} + <button + class={clsx( + "flex outline-none justify-center items-center gap-1 w-full h-full", + )} + disabled={!header.column.getCanSort()} + on:click={header.column.getToggleSortingHandler()} + > + <svelte:component + this={typeAnnotateComponent( + flexRender( + header.column.columnDef + .header, + header.getContext(), + ), + )} + /> + <span + >{getSortingSymbol( + header.column.getIsSorted(), + )}</span + > + </button> + {/if} + </th> + {/each} + </tr> + {/each} + </thead> + <tbody> + {#each $table.getRowModel().rows as row} + <tr + class="cursor-pointer hover:bg-slate-50 transition-colors duration-100" + > + <td + class="p-1 px-4 border border-t-0 border-r-0 text-center grid place-items-center h-full w-20" + > + <div> + <Checkbox + bind:checked={$checkMap[row.original.id]} + /> + </div> + </td> + {#each row.getVisibleCells() as cell} + <td class="p-1 px-4 border text-center"> + <svelte:component + this={typeAnnotateComponent( + flexRender( + cell.column.columnDef.cell, + cell.getContext(), + ), + )} + /> + </td> + {/each} + </tr> + {/each} + </tbody> + </table> + </div> + + {#if !updatingPaginationState && rowCount > 0} + <div class="w-full py-2"> + <Pagination + count={rowCount} + page={$table.getState().pagination.pageIndex + 1} + perPage={$table.getState().pagination.pageSize} + siblingCount={1} + setPage={setCurrentPage} + /> + </div> + {/if} +</div> diff --git a/src/routes/admin/post-data-panel/stores.ts b/src/routes/admin/post-data-panel/stores.ts new file mode 100644 index 0000000..ae2e2e3 --- /dev/null +++ b/src/routes/admin/post-data-panel/stores.ts @@ -0,0 +1,45 @@ +import type { DDFilters, PresetDataEntry } from "$lib/utils/data.types"; +import { writable } from "svelte/store"; +import { + type TableOptions, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, +} from "@tanstack/svelte-table"; + +export const filters = writable<DDFilters>({ + date: new Date().toISOString().split("T")[0], + draw: undefined, +}); + +export const checkMap = writable<Record<string, boolean>>({}); + +export const presetDataTableOptions = writable<TableOptions<PresetDataEntry>>({ + data: [], + columns: [], + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableGlobalFilter: true, +}); + +export const setPresetDataTableData = ( + data: PresetDataEntry[], + extend: boolean = false, +) => { + checkMap.update((state) => { + const map = {} as Record<string, boolean>; + for (const each of data) { + map[each.id] = false; + } + return map; + }); + presetDataTableOptions.update((state) => { + return { + ...state, + data: extend ? [...state.data, ...data].reverse() : data.reverse(), + }; + }); +}; diff --git a/src/routes/admin/post-data-preview-table.svelte b/src/routes/admin/post-data-preview-table.svelte new file mode 100644 index 0000000..1ce2491 --- /dev/null +++ b/src/routes/admin/post-data-preview-table.svelte @@ -0,0 +1,248 @@ +<script lang="ts"> + import type { PostDataEntry } from "$lib/utils/data.types"; + import { + createColumnHelper, + createSvelteTable, + flexRender, + type SortDirection, + } from "@tanstack/svelte-table"; + import { postDataTableOptions } from "./stores"; + import Select from "$lib/components/atoms/select.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import { clsx } from "clsx"; + import Pagination from "$lib/components/atoms/pagination.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + + const minPageSize = 100; + + export let rowCount: number; + let updatingPaginationState = false; + let pagination = { pageSize: minPageSize, pageIndex: 0 }; + let searchQuery = ""; + + const getSortingSymbol = (isSorted: boolean | SortDirection) => { + return isSorted === "asc" ? "▲" : isSorted === "desc" ? "▼" : ""; + }; + + const setCurrentPage = (page: number) => { + postDataTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageIndex: page, + pageSize: state.state?.pagination?.pageSize ?? minPageSize, + }, + }, + })); + }; + const setPaginationPageSize = (size: number) => { + updatingPaginationState = true; + setCurrentPage(0); + postDataTableOptions.update((state) => ({ + ...state, + state: { + ...state.state, + pagination: { + pageSize: size, + pageIndex: state.state?.pagination?.pageIndex ?? 0, + }, + }, + })); + setTimeout(() => { + updatingPaginationState = false; + }, 100); + }; + + const colHelper = createColumnHelper<PostDataEntry>(); + const columns = [ + colHelper.display({ + id: "number", + cell: (info) => info.row.original.number, + header: "No", + // @ts-ignore + accessorFn: (r) => r.number.toString(), + }), + colHelper.display({ + id: "first", + cell: (info) => info.row.original.first, + header: "First", + // @ts-ignore + accessorFn: (r) => r.first.toString(), + }), + colHelper.display({ + id: "second", + cell: (info) => info.row.original.second, + header: "Second", + // @ts-ignore + accessorFn: (r) => r.second.toString(), + }), + ]; + + let searchTimeOut: NodeJS.Timeout; + const handleEntrySearch = (e: any) => { + clearTimeout(searchTimeOut); + searchTimeOut = setTimeout(() => { + const value = e.target.value; + searchQuery = value; + postDataTableOptions.update((opts: any) => { + return { + ...opts, + state: { + ...opts.state, + columnFilters: [{ id: "number", value }], + }, + }; + }); + }, 300); + }; + + const typeAnnotateComponent = (a: any) => { + return a as any; + }; + + const table = createSvelteTable(postDataTableOptions); + + onMount(() => { + postDataTableOptions.update((p) => { + return { + ...p, + columns, + state: { ...p.state, pagination }, + }; + }); + setPaginationPageSize(minPageSize); + }); +</script> + +<!-- INFO: The table itself --> +<div class="w-full text-black"> + <div class="my-2 flex w-full justify-between items-center gap-4"> + <div class="flex justify-end md:max-w-xs w-full"> + <Input + label={"Search"} + placeholder={"Type anything"} + fieldWidth={"full"} + onInput={handleEntrySearch} + /> + </div> + <div + class="flex justify-between flex-col w-full md:flex-row md:max-w-xs gap-2" + > + <Select + fullWidth + label={"Row count"} + options={[100, 250, 500, 1000].map((n) => { + return { + id: n.toString(), + label: n.toString(), + value: n.toString(), + }; + })} + onSelect={(e) => { + // @ts-ignore + const chosen = Number(e.target.value); + setPaginationPageSize(chosen); + }} + /> + </div> + </div> + + <div class="w-full overflow-y-auto h-[69vh]"> + <table class="w-full"> + <thead> + {#each $table.getHeaderGroups() as headerGroup} + <tr> + {#each headerGroup.headers as header} + <th + class="p-2 px-4 border sticky top-0 bg-sky-50 text-sky-700" + colSpan={header.colSpan} + > + {#if !header.isPlaceholder} + <button + class={clsx( + "flex outline-none justify-center items-center gap-1 w-full h-full", + )} + disabled={!header.column.getCanSort()} + on:click={header.column.getToggleSortingHandler()} + > + <svelte:component + this={typeAnnotateComponent( + flexRender( + header.column.columnDef + .header, + header.getContext(), + ), + )} + /> + <span + >{getSortingSymbol( + header.column.getIsSorted(), + )}</span + > + </button> + {/if} + </th> + {/each} + </tr> + {/each} + </thead> + <tbody> + {#each $table.getRowModel().rows as row} + <tr + class="cursor-pointer hover:bg-slate-50 transition-colors duration-100" + on:click={() => { + // fetchFSRow(row.original.number); + }} + > + {#each row.getVisibleCells() as cell} + <td class="p-1 px-4 border text-center"> + <svelte:component + this={typeAnnotateComponent( + flexRender( + cell.column.columnDef.cell, + cell.getContext(), + ), + )} + /> + </td> + {/each} + </tr> + {/each} + </tbody> + </table> + </div> + + {#if !updatingPaginationState} + <div class="w-full py-2"> + <Pagination + count={rowCount} + page={$table.getState().pagination.pageIndex + 1} + perPage={$table.getState().pagination.pageSize} + siblingCount={1} + setPage={setCurrentPage} + /> + </div> + {/if} + + {#if searchQuery.length > 0} + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Search Total :"} theme={"sky"} /> + <Pill + text={"F : " + + $table + .getRowModel() + .rows.reduce((a, b) => a + b.original.first, 0)} + theme={"slate"} + /> + + <Pill + text={"S : " + + $table + .getRowModel() + .rows.reduce((a, b) => a + b.original.second, 0)} + theme={"slate"} + /> + </div> + {/if} +</div> diff --git a/src/routes/admin/post-data-summary-section.svelte b/src/routes/admin/post-data-summary-section.svelte new file mode 100644 index 0000000..9336785 --- /dev/null +++ b/src/routes/admin/post-data-summary-section.svelte @@ -0,0 +1,88 @@ +<script lang="ts"> + import CenteredSpinner from "$lib/components/molecules/centered-spinner.svelte"; + import type { TRPC } from "$lib/trpc/trpc"; + import type { Draw } from "$lib/utils/data.types"; + import toast from "svelte-french-toast"; + import PostDataPreviewTable from "./post-data-preview-table.svelte"; + import { setPostDataTableData } from "./stores"; + import Pill from "$lib/components/atoms/pill.svelte"; + import { postdata } from "./stores"; + + export let api: TRPC; + export let payload: { + date: string; + draw: Draw | undefined; + minPrize: number; + maxPrize: number; + }; + + console.log(payload); + + let affectingRowVisibility = false; + + let fetchPostDataQ = api.postData.getPostDataForPreview.createQuery( + { + date: payload.date, + draw: payload.draw, + minPrize: payload.minPrize, + maxPrize: payload.maxPrize, + twoDigitRates: { + first: payload.draw?.abRateF ?? 0, + second: payload.draw?.abRateS ?? 0, + }, + threeDigitRates: { + first: payload.draw?.abcRateF ?? 0, + second: payload.draw?.abcRateS ?? 0, + }, + customData: "", + }, + { + refetchOnWindowFocus: false, + onSuccess: (d) => { + if (!d.ok) { + toast.error(d.detail); + postdata.set([]); + return; + } + postdata.set(d.data); + affectingRowVisibility = false; + toast("Data fetched successfully."); + }, + onError: (e) => { + console.error(e); + toast( + "An error occurred while fetching data. Try again after a page refresh.", + ); + }, + }, + ); + + $: isLoading = $fetchPostDataQ.isLoading || $fetchPostDataQ.isFetching; + + postdata.subscribe((d) => { + setPostDataTableData(d); + }); + + $: totals = { + f: $postdata.reduce((acc, d) => acc + d.first, 0), + s: $postdata.reduce((acc, d) => acc + d.second, 0), + }; +</script> + +{#if isLoading} + <CenteredSpinner /> +{:else if $postdata.length > 0 && !affectingRowVisibility} + <PostDataPreviewTable rowCount={$postdata.length} /> + + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Totals :"} theme={"sky"} /> + <Pill text={`F rate : ${totals.f}`} theme={"slate"} /> + <Pill text={`S rate : ${totals.s}`} theme={"slate"} /> + </div> +{:else} + <div class="flex justify-center items-center h-64"> + <p class="text-gray-500 text-lg"> + No data generated for posting from the given filters + </p> + </div> +{/if} diff --git a/src/routes/admin/stores.ts b/src/routes/admin/stores.ts new file mode 100644 index 0000000..aadbaa3 --- /dev/null +++ b/src/routes/admin/stores.ts @@ -0,0 +1,32 @@ +import type { Draw, PostDataEntry } from "$lib/utils/data.types"; +import { writable } from "svelte/store"; +import { + type TableOptions, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, +} from "@tanstack/svelte-table"; + +export const nowDT = writable<Date>(new Date()); + +export const filters = writable<{ date: string; draw: Draw | undefined }>({ + date: new Date().toISOString().split("T")[0], + draw: undefined, +}); + +export const postdata = writable<PostDataEntry[]>([]); + +export const postDataTableOptions = writable<TableOptions<PostDataEntry>>({ + data: [], + columns: [], + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableGlobalFilter: false, +}); + +export const setPostDataTableData = (data: PostDataEntry[]) => { + postDataTableOptions.update((state) => ({ ...state, data })); +}; diff --git a/src/routes/admin/user-data/+page.svelte b/src/routes/admin/user-data/+page.svelte new file mode 100755 index 0000000..181e4c9 --- /dev/null +++ b/src/routes/admin/user-data/+page.svelte @@ -0,0 +1,294 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import Line from "$lib/components/atoms/line.svelte"; + import Pill from "$lib/components/atoms/pill.svelte"; + import Select from "$lib/components/atoms/select.svelte"; + import Title from "$lib/components/atoms/title.svelte"; + import { trpc } from "$lib/trpc/trpc"; + import type { ApiUser, BookingEntry, Draw } from "$lib/utils/data.types"; + import clsx from "clsx"; + import toast from "svelte-french-toast"; + import { + createColumnHelper, + createSvelteTable, + getCoreRowModel, + type TableOptions, + flexRender, + getFilteredRowModel, + type FilterFn, + } from "@tanstack/svelte-table"; + import { writable } from "svelte/store"; + import { rankItem } from "@tanstack/match-sorter-utils"; + + const api = trpc(); + + let chosenDate = new Date().toISOString().split("T")[0]; + let chosenDraw = {} as Draw; + let chosenDealer = {} as ApiUser; + let userListQuery = ""; + let showFiltersList = false; + let previousDataCount = 0; + + let globalFilter = ""; + const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ itemRank }); + return itemRank.passed; + }; + const setGlobalFilter = (filter: string) => { + console.log(filter); + globalFilter = filter; + options.update((p) => { + return { + ...p, + state: { ...p.state, globalFilter: filter }, + }; + }); + }; + const colHelper = createColumnHelper<BookingEntry>(); + const options = writable<TableOptions<BookingEntry>>({ + data: [], + columns: [ + colHelper.display({ + id: "number", + cell: (info) => info.row.original.number, + header: "No", + // @ts-ignore + accessorFn: (r) => r.number.toString(), + }), + colHelper.display({ + id: "first", + cell: (info) => info.row.original.first, + header: "F", + // @ts-ignore + accessorFn: (r) => r.first.toString(), + }), + colHelper.display({ + id: "second", + cell: (info) => info.row.original.second, + header: "S", + // @ts-ignore + accessorFn: (r) => r.second.toString(), + }), + ], + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + enableGlobalFilter: true, + state: { globalFilter }, + onGlobalFilterChange: setGlobalFilter, + }); + const table = createSvelteTable(options); + + let filtersQ = api.apiData.getDealersAndDraws.createQuery(undefined, { + refetchOnWindowFocus: false, + retry: 3, + onSuccess: (d) => { + if (d.draws.length > 0) { + chosenDraw = d.draws[0]; + } + return d; + }, + }); + + $: isLoading = $filtersQ.isLoading || $filtersQ.isFetching; + + let fetchDataM = api.apiData.getDataByFilters.createMutation({ + onSuccess: (data) => { + toast("Found " + data.data.length + " entries"); + previousDataCount = data.data.length; + options.update((p) => { + return { + ...p, + data: data.data, + }; + }); + }, + onError: (err) => { + console.log(err); + toast.error("An error occured while fetching data"); + }, + }); + + const typeAnnotateComponent = (a: any) => { + return a as any; + }; + + let timer: NodeJS.Timeout; + const handleEntrySearch = (e: any) => { + clearTimeout(timer); + timer = setTimeout(() => { + setGlobalFilter(e.target.value); + }, 300); + }; +</script> + +<section class="w-full grid place-items-center p-8"> + <div + class="w-full flex flex-col gap-4 max-w-6xl p-6 lg:p-8 rounded-lg shadow-sm border" + > + <div class="flex justify-between w-full items-center"> + <Title text={"User Data"} size={"h1"} /> + <Button + text={"Toggle filters"} + intent={"ghost"} + onClick={() => { + showFiltersList = !showFiltersList; + }} + /> + </div> + <Line /> + {#if isLoading} + <div class="w-max"> + <Pill theme={"slate"} text={"Fetching draws & users"} /> + </div> + {:else if $filtersQ.data && showFiltersList} + <section class="flex flex-col md:flex-row gap-4 w-full"> + <Input + bind:value={chosenDate} + label={"Date"} + inputType={"date"} + placeholder={"Date"} + /> + <Select + fullWidth + label={"Draw"} + options={$filtersQ.data.draws.map((e) => { + return { label: e.title, value: e.id, id: e.id }; + })} + onSelect={(e) => { + if ($filtersQ.data) { + // @ts-ignore + chosenDraw = $filtersQ.data.draws.find( + // @ts-ignore + (d) => d.id === e.target.value + ); + } + }} + /> + </section> + <div class="flex flex-col w-full gap-2"> + <Input + bind:value={userListQuery} + placeholder={"Search dealers . . ."} + /> + <div + class="p-2 rounded-lg w-full border shadow-sm h-80 overflow-y-auto flex flex-col gap-1" + > + {#each $filtersQ.data.users as user} + <!-- svelte-ignore a11y-no-static-element-interactions --> + {#if user && (userListQuery === "" || user.userName + .toLowerCase() + .includes(userListQuery.toLowerCase()) || user.userId + .toLowerCase() + .includes(userListQuery.toLowerCase()))} + <div + class={clsx( + "rounded-md hover:bg-sky-200 text-sky-800 font-medium p-2 cursor-pointer transition-colors duration-100 border capitalize", + chosenDealer.id === user.id + ? "bg-sky-300 border-sky-600" + : "bg-sky-50 border-sky-200" + )} + on:click={() => (chosenDealer = user)} + on:touchend={() => (chosenDealer = user)} + on:keypress={() => {}} + > + {user?.userName} - {user?.userId} + </div> + {/if} + {/each} + </div> + <div class="flex justify-end"> + <Button + text={"Apply filters"} + onClick={async () => { + if (!chosenDate || !chosenDraw.id || !chosenDealer.id) { + toast.error("Select all filters properly to fetch data"); + return; + } + showFiltersList = false; + toast("Fetching data for selected filters"); + await $fetchDataM.mutateAsync({ + drawId: chosenDraw.id ?? "", + date: chosenDate, + userId: chosenDealer.id ?? "", + }); + }} + /> + </div> + </div> + {:else if !showFiltersList} + <div class="flex w-full gap-2 flex-wrap"> + <Pill text={"Filters:"} theme={"slate"} /> + <Pill text={chosenDate} theme={"sky"} /> + <Pill text={chosenDraw.title} theme={"sky"} /> + <Pill + text={chosenDealer.id + ? `${chosenDealer.userName} - ${chosenDealer.userId}` + : "No dealer selected"} + theme={"sky"} + /> + </div> + {/if} + <Line /> + + {#if previousDataCount > 0} + <div class="w-full text-black"> + <div class="flex justify-end"> + <Input placeholder={"Search"} onInput={handleEntrySearch} /> + </div> + <div class="my-2 w-full" /> + <div class="w-full overflow-y-auto h-96 border"> + <table class="w-full"> + <thead> + {#each $table.getHeaderGroups() as headerGroup} + <tr> + {#each headerGroup.headers as header} + <th + class="p-1 px-4 border sticky top-0 bg-sky-50 text-sky-700" + colSpan={header.colSpan} + > + {#if !header.isPlaceholder} + <svelte:component + this={typeAnnotateComponent( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} + /> + {/if} + </th> + {/each} + </tr> + {/each} + </thead> + <tbody> + {#each $table.getRowModel().rows as row} + <tr> + {#each row.getVisibleCells() as cell} + <td class="p-1 px-4 border text-center"> + <svelte:component + this={typeAnnotateComponent( + flexRender( + cell.column.columnDef.cell, + cell.getContext() + ) + )} + /> + </td> + {/each} + </tr> + {/each} + </tbody> + </table> + </div> + <div>{$table.getRowModel().rows.length} Rows</div> + </div> + {:else} + <span class="w-full text-center py-24"> + <Title size={"h4"} text={"No data found for chosen filters"} /> + </span> + {/if} + </div> +</section> diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte new file mode 100755 index 0000000..17154b2 --- /dev/null +++ b/src/routes/admin/users/+page.svelte @@ -0,0 +1,7 @@ +<script lang="ts"> + import { trpc } from "$lib/trpc/trpc"; + + const api = trpc(); +</script> + +<span>show the users list here</span> diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts new file mode 100755 index 0000000..4c795e2 --- /dev/null +++ b/src/routes/api/auth/logout/+server.ts @@ -0,0 +1,17 @@ +import { deleteSession } from "$lib/server/session.helpers"; +import { constants } from "$lib/utils/constants"; +import type { RequestHandler } from "@sveltejs/kit"; + +export const POST = (async ({ cookies }) => { + const sidKey = constants.SESSION_KEY_NAME; + const sid = cookies.get(sidKey); + if (sid) { + await deleteSession(sid); + } + return new Response(null, { + status: 200, + headers: { + "set-cookie": `${sidKey}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`, + }, + }); +}) satisfies RequestHandler; diff --git a/src/routes/api/auth/signin/+server.ts b/src/routes/api/auth/signin/+server.ts new file mode 100755 index 0000000..a011bc1 --- /dev/null +++ b/src/routes/api/auth/signin/+server.ts @@ -0,0 +1,117 @@ +import type { RequestHandler } from "@sveltejs/kit"; +import type { ServerError } from "$lib/utils/data.types"; +import { constants } from "$lib/utils/constants"; +import { dbUser } from "$lib/server/db/user.db"; +import { compareData } from "$lib/server/hashing"; +import { generateSession } from "$lib/server/session.helpers"; +import { redis } from "$lib/server/connectors/redis"; +import { z } from "zod"; + +const COOLDOWN = 60 * 1; + +const cooldownStatus = async ( + username: string, + ip: string, + userAgent: string +) => { + const key = `cooldown:${username}:${ip}:${userAgent}`; + const res = await redis.get(key); + return res === null ? 0 : parseInt(res); +}; + +const add1ToCooldown = async ( + username: string, + ip: string, + userAgent: string +) => { + const key = `cooldown:${username}:${ip}:${userAgent}`; + if (await redis.exists(key)) { + await redis.incr(key); + } else { + await redis.setex(key, COOLDOWN, 1); + } +}; + +export const POST = (async ({ cookies, request }) => { + const jay = await request.json(); + const payloadSchema = z.object({ + username: z.string().min(4), + password: z.string().min(4), + }); + const result = payloadSchema.safeParse(jay); + if (!result.success) { + return new Response( + JSON.stringify({ + success: false, + errors: result.error.flatten().formErrors.map((e) => { + return { message: e }; + }), + }), + { headers: { "content-type": "application/json" }, status: 401 } + ); + } + const other = { + ip: request.headers.get("x-forwarded-for") || "", + userAgent: request.headers.get("user-agent") || "", + }; + const cooldown = await cooldownStatus( + result.data.username, + other.ip, + other.userAgent + ); + if (cooldown >= 5) { + return new Response( + JSON.stringify({ + success: false, + errors: [ + { + message: "Too many attempts, try again later", + meta: { duration: 1000 * COOLDOWN }, + }, + ], + }), + { headers: { "content-type": "application/json" }, status: 401 } + ); + } + const user = await dbUser.get({ username: result.data.username }); + if (!user || !compareData(result.data.password, user.password)) { + await add1ToCooldown(result.data.username, other.ip, other.userAgent); + return new Response( + JSON.stringify({ + success: false, + errors: [{ message: "Invalid username or password" }], + }), + { headers: { "content-type": "application/json" }, status: 401 } + ); + } + const sid = await generateSession( + user.username, + user.userType, + other.ip, + other.userAgent + ); + if (!sid) { + return new Response( + JSON.stringify({ + success: false, + errors: [{ message: "Invalid username or password" }], + }), + { headers: { "content-type": "application/json" }, status: 401 } + ); + } + cookies.set(constants.SESSION_KEY_NAME, sid, { + path: "/", + expires: new Date(Date.now() + constants.SESSION_EXPIRE_TIME_MS), + httpOnly: true, + secure: true, + }); + + return new Response( + JSON.stringify({ success: true, errors: [] as ServerError }), + { + headers: { + "content-type": "application/json", + }, + } + ); +}) satisfies RequestHandler; diff --git a/src/routes/api/auth/verify/+server.ts b/src/routes/api/auth/verify/+server.ts new file mode 100755 index 0000000..825b519 --- /dev/null +++ b/src/routes/api/auth/verify/+server.ts @@ -0,0 +1,16 @@ +import { constants } from "$lib/utils/constants"; +import type { RequestHandler } from "@sveltejs/kit"; + +export const GET = (async ({ cookies }) => { + const sidKey = constants.SESSION_KEY_NAME; + const sid = cookies.get(sidKey); + let status = 200; + let headers = {}; + if (!sid) { + status = 401; + headers = { + "set-cookie": `${sidKey}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`, + }; + } + return new Response(null, { status, headers }); +}) satisfies RequestHandler; diff --git a/src/routes/api/debug/user/+server.ts b/src/routes/api/debug/user/+server.ts new file mode 100755 index 0000000..b5be99f --- /dev/null +++ b/src/routes/api/debug/user/+server.ts @@ -0,0 +1,59 @@ +import { dbUser } from "$lib/server/db/user.db"; +import { hashData } from "$lib/server/hashing"; +import { zLooseUser } from "$lib/utils/data.types"; +import type { RequestHandler } from "@sveltejs/kit"; +import { z } from "zod"; + +export const POST = (async ({ request }) => { + if (request.headers.get("x-debug-key") !== process.env.DEBUG_KEY) { + return new Response("Unauthorized", { status: 401 }); + } + const jsonBody = await request.json(); + const schema = z.object({ operation: z.string(), data: z.array(zLooseUser) }); + const parsed = await schema.safeParseAsync(jsonBody); + if (!parsed.success) { + return new Response(JSON.stringify(parsed.error), { + status: 400, + headers: { "content-type": "application/json" }, + }); + } + const data = parsed.data; + console.log(data); + let payload: any = {}; + if (data.operation === "create") { + for (const user of data.data) { + const pwdHash = hashData(user.password); + payload[user.username] = await dbUser.create({ + password: pwdHash, + userType: user.userType, + username: user.username, + association: user.association, + }); + } + } else if (data.operation === "all") { + payload = await dbUser.all(); + } else if (data.operation === "read") { + for (const user of data.data) { + const un = user.username; + payload[un] = await dbUser.get({ username: un }); + } + } else if (data.operation === "update") { + // for (const user of data.data) { + // const un = user.username; + // payload[un] = await dbUser.update({ + // username: un, + // password: hashData(user.password), + // userType: user.userType, + // association: user.association, + // }); + // } + payload = { message: "NYI." }; + } else if (data.operation === "delete") { + payload = { message: "NYI" }; + } + + return new Response(JSON.stringify(payload), { + status: 200, + headers: { "content-type": "application/json" }, + }); +}) satisfies RequestHandler; diff --git a/src/routes/auth/signin/+page.svelte b/src/routes/auth/signin/+page.svelte new file mode 100755 index 0000000..b04a0f7 --- /dev/null +++ b/src/routes/auth/signin/+page.svelte @@ -0,0 +1,13 @@ +<script lang="ts"> + import clsx from "clsx"; + + import Signinform from "./signinform.svelte"; +</script> + +<section + class={clsx( + "grid place-items-center bg-[url('/images/signin-bg.svg')] bg-no-repeat bg-cover w-screen h-screen" + )} +> + <Signinform /> +</section> diff --git a/src/routes/auth/signin/signinform.svelte b/src/routes/auth/signin/signinform.svelte new file mode 100755 index 0000000..e6ff601 --- /dev/null +++ b/src/routes/auth/signin/signinform.svelte @@ -0,0 +1,162 @@ +<script lang="ts"> + import Button from "$lib/components/atoms/button.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import toast from "svelte-french-toast"; + import Title from "$lib/components/atoms/title.svelte"; + import { goto } from "$app/navigation"; + import type { EventHandler } from "@melt-ui/svelte/internal/helpers"; + + let loginDisabled = false; + let signingIn = false; + + const errors = { + username: { + error: false, + message: "", + validator: (value: string) => { + if (value.length < 4) { + return "Username must be at least 4 characters long"; + } + if (value.length > 64) { + return "Username must be at most 64 characters long"; + } + return ""; + }, + }, + password: { + error: false, + message: "", + validator: (value: string) => { + if (value.length < 6) { + return "Password must be at least 6 characters long"; + } + if (value.length > 128) { + return "Password must be at most 128 characters long"; + } + return ""; + }, + }, + pin: { + error: false, + message: "", + validator: (value: string) => { + if (value.length !== 6) { + return "Pin must be 6 characters long"; + } + return ""; + }, + }, + }; + + const setCooldown = (meta: any) => { + try { + loginDisabled = true; + window.localStorage.setItem("cooldown", JSON.stringify(meta.duration)); + setTimeout(() => { + loginDisabled = false; + window.localStorage.removeItem("cooldown"); + }, meta.duration); + } catch (err) {} + }; + + const ensureCooldown = () => { + try { + if (window) { + const cooldown = JSON.parse( + window.localStorage.getItem("cooldown") ?? "" + ); + if (cooldown) { + loginDisabled = true; + toast.error("Too many attempts, try again in 1 minute"); + setTimeout(() => { + loginDisabled = false; + window.localStorage.removeItem("cooldown"); + }, cooldown); + } + } + } catch (err) {} + }; + + const signInHandler: EventHandler = async (e) => { + signingIn = true; + const formData = new FormData(e.target as HTMLFormElement); + const json = Object.fromEntries(formData.entries()); + const res = await fetch("/api/auth/signin", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(json), + }); + signingIn = false; + if (res.status === 200) { + toast.success("Successfully signed in"); + goto("/"); + } else { + const rJ = await res.json(); + if (rJ.errors.length > 0) { + for (const each of rJ.errors) { + toast.error(each.message); + if (each.message.includes("Too many attempts")) { + setCooldown(each.meta); + } + } + } + } + }; + + onMount(() => { + ensureCooldown(); + }); +</script> + +<form + class="z-10 flex w-full max-w-sm flex-col backdrop-blur-lg justify-center gap-3 rounded-md border-gray-400 bg-white bg-opacity-80 p-8 md:p-12 py-16 shadow-lg" + on:submit|preventDefault={signInHandler} +> + <Title text={"Sign In"} size={"h2"} /> + <small class="text-gray-500"> Enter your credentials to get started </small> + + <Input + name={"username"} + label={"Username"} + placeholder={"Username"} + intent={"primary"} + fieldWidth={"full"} + bordered={"yes"} + otherInputOptions={{ minLength: 4, maxLength: 64, required: true }} + isError={errors.username.error} + bottomLabel={errors.username.message} + onInput={(e) => { + // @ts-ignore + const value = e.target.value; + const message = errors.username.validator(value); + errors.username.error = message.length > 0; + errors.username.message = message; + }} + /> + <Input + name={"password"} + fieldWidth={"full"} + label={"Password"} + placeholder={"Password"} + inputType={"password"} + intent={"primary"} + bordered={"yes"} + otherInputOptions={{ minLength: 6, maxLength: 128, required: true }} + isError={errors.password.error} + bottomLabel={errors.password.message} + onInput={(e) => { + // @ts-ignore + const value = e.target.value; + const message = errors.password.validator(value); + errors.password.error = message.length > 0; + errors.password.message = message; + }} + /> + <div class="my-1 w-full" /> + <Button + disabled={signingIn || loginDisabled} + text={signingIn ? "Validating..." : "Sign In"} + intent={signingIn ? "ghost" : "primary"} + fullwidth={"yes"} + /> +</form> diff --git a/src/routes/user/+layout.svelte b/src/routes/user/+layout.svelte new file mode 100755 index 0000000..4fa864c --- /dev/null +++ b/src/routes/user/+layout.svelte @@ -0,0 +1 @@ +<slot /> diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte new file mode 100755 index 0000000..cc50468 --- /dev/null +++ b/src/routes/user/+page.svelte @@ -0,0 +1,208 @@ +<script lang="ts"> + import { + bookingPanelData, + bookingPanelState, + removeEntriesFromSyncState, + selectedEntriesMap, + syncState, + type SyncState, + } from "$lib/stores/booking.state"; + import { trpc } from "$lib/trpc/trpc"; + import { SCHEMES } from "$lib/utils/constants"; + + import DataEntryView from "./components/data-entry-view.svelte"; + import SchemesSection from "./components/schemes-section.svelte"; + import UserNavbar from "./user-navbar.svelte"; + import CenteredSpinner from "$lib/components/molecules/centered-spinner.svelte"; + import DataEntrySection from "./components/data-entry-section.svelte"; + import toast from "svelte-french-toast"; + + const api = trpc(); + + let fetchingData = false; + let previousDrawId = ""; + + let bookingDataQ = api.booking.getBookingData.createMutation({ + retry: 3, + onSuccess: (data) => { + fetchingData = false; + selectedEntriesMap.update((_) => { + const out = {} as Record<string, boolean>; + for (const each of data.data) { + out[each.id] = false; + } + return out; + }); + bookingPanelData.update((_) => { + return data.data; + }); + }, + onError: (err) => { + fetchingData = false; + console.log(err); + }, + }); + + let sessionQ = api.session.getSession.createQuery(); + + let syncbookingM = api.booking.syncBooking.createMutation({ + retry: 3, + retryDelay: 1000 * 1, + }); + let deleteBookingM = api.booking.deleteBooking.createMutation({ + retry: 3, + retryDelay: 1000 * 1, + }); + + bookingPanelState.subscribe((state) => { + const newId = state.chosenDraw.id; + if (newId && newId !== previousDrawId) { + previousDrawId = newId; + triggerDataFetch(); + } + }); + + let [defaultIntervalDuration, intervalDuration] = [1000 * 3, 1000 * 3]; + let syncInterval: NodeJS.Timeout; + + async function triggerDataFetch() { + fetchingData = true; + if ($syncState.data.length > 0) { + await performSync(); + } + setTimeout(() => { + $bookingDataQ.mutate({ drawId: previousDrawId }); + }, 100); + selectedEntriesMap.update((_) => ({})); + bookingPanelData.update((_) => []); + } + + async function deleteSelectedEntries() { + const chosen = [] as string[]; + for (const [key, value] of Object.entries($selectedEntriesMap)) { + if (value) { + chosen.push(key); + } + } + if (chosen.length < 1) { + return; + } + fetchingData = true; + const out = await $deleteBookingM.mutateAsync({ bookingIds: chosen }); + if (out.detail) { + toast(out.detail); + bookingPanelData.update((old) => { + const nd = old.filter((e) => { + return !chosen.includes(e.id); + }); + selectedEntriesMap.update((_) => { + const o = {} as Record<string, boolean>; + for (const each of nd) { + o[each.id] = false; + } + return o; + }); + return nd; + }); + } + setTimeout(() => { + fetchingData = false; + }, 100); + } + + async function performSync() { + return new Promise(function (resolve, reject) { + $syncbookingM + .mutateAsync({ data: $syncState.data }) + .then((out) => { + removeEntriesFromSyncState(out.syncedEntriesIds); + resolve(null); + }) + .catch((err) => { + reject(err); + }); + }); + } + + function setupSync() { + async function syncFn() { + clearInterval(syncInterval); + if ($syncState.data.length > 0) { + toast.promise( + performSync(), + { + loading: "Syncing", + success: "Data synced", + error: "Could not sync, please refresh", + }, + { position: "top-right" } + ); + } + intervalDuration *= 2; + syncInterval = setInterval(syncFn, intervalDuration); + } + if (syncInterval) { + clearInterval(syncInterval); + } + intervalDuration = defaultIntervalDuration; + syncInterval = setInterval(syncFn, defaultIntervalDuration); + } + + onMount(() => { + setupSync(); + try { + const data = JSON.parse( + window.localStorage.getItem("syncstate") ?? "" + ) as SyncState; + if (!data || data.data.length < 1) { + return; + } + syncState.update((old) => { + return { ...old, ...data }; + }); + } catch (e) {} + }); + + onDestroy(() => { + if (syncInterval) { + clearInterval(syncInterval); + } + }); +</script> + +<UserNavbar + username={$sessionQ.data ? $sessionQ.data.user.username : ". . ."} + {api} +/> + +{#if fetchingData} + <section + class="fixed left-0 top-0 w-screen h-screen grid place-items-center bg-sky-800 bg-opacity-20 z-50" + > + <CenteredSpinner /> + </section> +{/if} + +<section class="w-full grid place-items-center p-8"> + <div class="grid grid-cols-4 h-[80vh] gap-4 w-full"> + <SchemesSection + title={"Combination Schemes"} + schemes={SCHEMES.permutations} + isPermutationMode={true} + highLightIfPermutationMode={true} + /> + <div class="col-span-2"> + <DataEntryView + on:refreshData={() => triggerDataFetch()} + on:deleteSelectedEntries={deleteSelectedEntries} + /> + </div> + <SchemesSection + title={"Game Schemes"} + schemes={SCHEMES.permutations} + isPermutationMode={false} + highLightIfPermutationMode={false} + /> + </div> +</section> +<DataEntrySection on:refreshSync={setupSync} /> diff --git a/src/routes/user/components/data-entry-input-field.svelte b/src/routes/user/components/data-entry-input-field.svelte new file mode 100755 index 0000000..dffdf27 --- /dev/null +++ b/src/routes/user/components/data-entry-input-field.svelte @@ -0,0 +1,41 @@ +<script lang="ts"> + import clsx from "clsx"; + import { getUUID } from "$lib/utils"; + import { COLOR_TRANSITION } from "$lib/utils/constants"; + + export let name: string; + export let placeholder: string; + export let value: string; + export let disabled: boolean = false; + export let otherInputOptions: Record<string, string | number | boolean> = {}; + export let maxlength: number = 10; + export let onInput: (v: string) => void = (_) => {}; + export let onKeyPress = (_: KeyboardEvent) => {}; + export let width = "w-24"; + export let bindNode: HTMLInputElement; + + const _onInput = (e: Event) => { + const target = e.target as HTMLInputElement; + onInput(target.value); + }; + const inputId = getUUID(); +</script> + +<input + id={inputId} + {name} + {placeholder} + {maxlength} + bind:this={bindNode} + bind:value + on:keypress={onKeyPress} + on:input={_onInput} + autocomplete="off" + disabled={disabled ?? false} + class={clsx( + COLOR_TRANSITION, + width, + "outline-none p-1 px-2 disabled:cursor-not-allowed cursor-text bg-slate-50 hover:bg-sky-50 active:bg-sky-50 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700 border-2 border-slate-400 rounded-md hover:border-sky-500 focus:border-sky-600 font-mono tracking-wide" + )} + {...otherInputOptions} +/> diff --git a/src/routes/user/components/data-entry-section.svelte b/src/routes/user/components/data-entry-section.svelte new file mode 100755 index 0000000..317d101 --- /dev/null +++ b/src/routes/user/components/data-entry-section.svelte @@ -0,0 +1,384 @@ +<script lang="ts"> + import Line from "$lib/components/atoms/line.svelte"; + import Switch from "$lib/components/atoms/switch.svelte"; + import { + bookingPanelData, + bookingPanelState, + resetSchemeStatus, + setBookingPanelState, + syncState, + } from "$lib/stores/booking.state"; + import clsx from "clsx"; + import IconJamChevronsCircleDownRight from "~icons/jam/chevrons-circle-down-right"; + import DataEntryInputField from "./data-entry-input-field.svelte"; + import LinkButton from "$lib/components/atoms/link-button.svelte"; + import { isDrawClosed } from "$lib/utils/datetime.helper.utils"; + import { + parseNumberInput, + parseRate, + } from "$lib/utils/booking/data.entry.helpers"; + import { LEXICODES_SORTED_FOR_INPUT } from "$lib/utils/constants"; + import type { BookingInputValues } from "$lib/utils/data.types"; + import { + ensureInputIsValid, + getParsedBookingEntries, + } from "$lib/utils/booking/booking.sync"; + import toast from "svelte-french-toast"; + + const dispatcher = createEventDispatcher(); + + let isExpanded = false; + const dataDomFields = { + number: undefined as unknown as HTMLInputElement, + default: { + first: undefined as unknown as HTMLInputElement, + second: undefined as unknown as HTMLInputElement, + }, + first: {} as Record<string, HTMLInputElement>, + second: {} as Record<string, HTMLInputElement>, + }; + const values = { + number: "", + default: { first: "", second: "" }, + first: {} as Record<string, string>, + second: {} as Record<string, string>, + } as BookingInputValues; + + function resetInputFieldValues() { + values.number = ""; + if (!$bookingPanelState.keepRatesPostBookingSubmit) { + values.default.first = ""; + values.default.second = ""; + for (const lc of LEXICODES_SORTED_FOR_INPUT) { + values.first[lc] = ""; + values.second[lc] = ""; + } + } + dataDomFields.number.focus(); + } + + for (const lc of LEXICODES_SORTED_FOR_INPUT) { + values.first[lc] = ""; + values.second[lc] = ""; + } + + $: totalBooking = $bookingPanelData.reduce( + (a, b) => a + b.first + b.second, + 0 + ); + let inputFieldWidth = 6; + $: maxSingleNoLen = $bookingPanelState.isPermutationModeSelected ? 8 : 4; + $: idc = $bookingPanelState.chosenDraw.id + ? isDrawClosed($bookingPanelState.chosenDraw.closeTime) + : false; + $: chosenLexiCodes = $bookingPanelState.chosenScheme + .split(".") + .filter((e) => e.length > 0); + $: shouldFlex = chosenLexiCodes.length > 3 || inputFieldWidth > 30; + + // to reset the rates on scheme change + bookingPanelState.subscribe((state) => { + if (chosenLexiCodes && state.chosenScheme !== chosenLexiCodes.join(".")) { + for (const lc of LEXICODES_SORTED_FOR_INPUT) { + values.first[lc] = ""; + values.second[lc] = ""; + } + } + }); + + function focusOnNumberInput() { + setTimeout(() => { + dataDomFields.number.focus(); + }, 250); + } + + function focusOnNextValidField() { + function isEmpty(el: HTMLInputElement) { + return el.value.length === 0; + } + const focusedElId = (window.document.activeElement as HTMLInputElement).id; + const keepRates = $bookingPanelState.keepRatesPostBookingSubmit; + if (isEmpty(dataDomFields.number)) { + dataDomFields.number.focus(); + return false; + } + let lastVisibleElId = dataDomFields.number.id; + if (chosenLexiCodes.length === 0) { + if (isEmpty(dataDomFields.default.first)) { + dataDomFields.default.first.focus(); + return false; + } + lastVisibleElId = dataDomFields.default.first.id; + let is2ndEmpty = isEmpty(dataDomFields.default.second); + if ( + is2ndEmpty || + (!is2ndEmpty && + focusedElId !== dataDomFields.default.second.id && + !keepRates) + ) { + dataDomFields.default.second.focus(); + return false; + } + lastVisibleElId = dataDomFields.default.second.id; + } + + for (const each of ["first", "second"] as const) { + for (const lc of chosenLexiCodes) { + let isVisible = dataDomFields[each][lc].checkVisibility(); + if (!isVisible) { + continue; + } + lastVisibleElId = dataDomFields[each][lc].id; + if (isEmpty(dataDomFields[each][lc])) { + dataDomFields[each][lc].focus(); + return false; + } + } + } + if ($bookingPanelState.keepRatesPostBookingSubmit) { + return true; + } + // NOW this part handles focusing on the "next" field to get to the last field + const _nodes = [dataDomFields.number]; + for (const each of ["first", "second"] as const) { + for (const lc of chosenLexiCodes) { + _nodes.push(dataDomFields[each][lc]); + } + } + const upper = _nodes.length - 1; + for (let i = 1; i <= upper; i++) { + const el = _nodes[i]; + if (!el?.checkVisibility()) { + continue; + } + // means we're focused on the last field, so skip + if (i === _nodes.length - 1 && focusedElId === el.id) { + break; + } + const prevEl = _nodes[i - 1]; + if (focusedElId === prevEl.id) { + el.focus(); + break; + } + } + return true && focusedElId === lastVisibleElId; + } + + function book() { + const chosenLexiCodes = $bookingPanelState.chosenScheme + .split(".") + .filter((e) => e.length > 0); + const errors = ensureInputIsValid(values, chosenLexiCodes); + if (errors.length > 0) { + for (const err of errors) { + toast.error(err.message); + } + return; + } + const data = getParsedBookingEntries( + values, + chosenLexiCodes, + $bookingPanelState.isPermutationModeSelected, + parseInt($bookingPanelState.chosenDraw.id.split(":")[1]) + ); + bookingPanelData.update((old) => { + old.unshift(...data); + return old; + }); + syncState.update((old) => { + old.data.push(...data); + return old; + }); + resetInputFieldValues(); + } + + function handleKeyPress(e: KeyboardEvent, inputType?: "no" | "rate") { + if (e.key === "Enter") { + const allPopulated = focusOnNextValidField(); + // remove the ending dot if any on enter + if ( + dataDomFields.number.value[dataDomFields.number.value.length - 1] === + "." + ) { + dataDomFields.number.value = dataDomFields.number.value.substring( + 0, + dataDomFields.number.value.length - 1 + ); + } + if (allPopulated) { + dispatcher("refreshSync"); + book(); + } + } + if (inputType === "no") { + // resize the input to be able to fit the number + const el = e.target as HTMLInputElement; + const len = el.value.length; + inputFieldWidth = parseFloat((len * 0.8).toFixed(2)); + el.style.width = `clamp(6rem, ${inputFieldWidth}rem, 75vw)`; + } + } + + onMount(() => { + window.addEventListener("keypress", (e) => { + try { + if (e.key === "d") { + isExpanded = false; + focusOnNumberInput(); + } else if (e.key === "u") { + isExpanded = true; + focusOnNumberInput(); + } + } catch (e) { + console.log(e); + } + }); + }); +</script> + +<section + class={clsx( + "fixed bottom-0 w-full z-50 transition-transform", + isExpanded ? "" : shouldFlex ? "translate-y-[86%]" : "translate-y-[78%]" + )} +> + <button + class="rounded-t-lg w-12 p-1 grid place-items-center bg-sky-100 text-sky-500 ml-4 hover:bg-sky-500 hover:text-sky-50 transition-colors duration-100 cursor-pointer" + on:click={() => { + isExpanded = !isExpanded; + }} + > + <IconJamChevronsCircleDownRight + class={clsx( + "w-8 h-8 transition-transform", + isExpanded ? "" : "rotate-[270deg]" + )} + /> + </button> + <div + class="rounded-t-xl w-full p-4 border-t border-x border-sky-500 bg-white flex flex-col gap-4" + > + <div + class={clsx( + "flex w-full gap-2 items-start", + shouldFlex ? "flex-col" : "flex-row" + )} + > + <div class="flex gap-2 items-start flex-wrap"> + <span class="w-5">No</span> + <DataEntryInputField + bind:bindNode={dataDomFields.number} + width={"w-24"} + name={"number"} + disabled={idc} + bind:value={values.number} + placeholder={"No"} + maxlength={10_000} + onKeyPress={(e) => handleKeyPress(e, "no")} + onInput={(v) => { + const parsed = parseNumberInput( + v, + maxSingleNoLen, + $bookingPanelState.isPermutationModeSelected + ); + values.number = parsed; + }} + /> + </div> + <div class="flex gap-2 items-start flex-wrap"> + <span class="w-5">F</span> + {#if chosenLexiCodes.length > 0} + {#each LEXICODES_SORTED_FOR_INPUT as lc} + {#if chosenLexiCodes.find((e) => e.toLowerCase() === lc.toLowerCase())} + <DataEntryInputField + name={lc} + disabled={idc} + bind:bindNode={dataDomFields.first[lc]} + bind:value={values.first[lc]} + placeholder={`${lc.toUpperCase()}`} + onKeyPress={(e) => handleKeyPress(e)} + onInput={(v) => { + values.first[lc] = parseRate(v); + }} + /> + {/if} + {/each} + {:else} + <DataEntryInputField + name={"F"} + bind:bindNode={dataDomFields.default.first} + bind:value={values.default.first} + disabled={idc} + placeholder={`F`} + onKeyPress={(e) => handleKeyPress(e)} + onInput={(v) => { + values.default.first = parseRate(v); + }} + /> + {/if} + </div> + <div class="flex gap-2 items-start flex-wrap"> + <span class="w-5">S</span> + {#if chosenLexiCodes.length > 0} + {#each LEXICODES_SORTED_FOR_INPUT as lc} + {#if chosenLexiCodes.find((e) => e.toLowerCase() === lc.toLowerCase())} + <DataEntryInputField + name={lc} + bind:bindNode={dataDomFields.second[lc]} + bind:value={values.second[lc]} + disabled={idc} + placeholder={`${lc.toUpperCase()}`} + onKeyPress={(e) => handleKeyPress(e)} + onInput={(v) => { + values.second[lc] = parseRate(v); + }} + /> + {/if} + {/each} + {:else} + <DataEntryInputField + name={"S"} + bind:value={values.default.second} + bind:bindNode={dataDomFields.default.second} + disabled={idc} + placeholder={`S`} + onKeyPress={(e) => handleKeyPress(e)} + onInput={(v) => { + values.default.second = parseRate(v); + }} + /> + {/if} + </div> + </div> + + <Line /> + <div class="w-full flex justify-between"> + <div class="flex items-center gap-2 text-sm"> + <p class="font-semibold tracking-wide text-sky-700"> + Total Booking : {totalBooking} + </p> + | + <p class="font-semibold tracking-wide text-sky-700"> + Entries : {$bookingPanelData.length} + </p> + </div> + <div class="flex gap-2 items-center"> + <LinkButton + text={"Reset Scheme"} + onClick={() => { + values.number = ""; + values.default.first = ""; + values.default.second = ""; + resetSchemeStatus(); + }} + /> + <Switch + label={"Keep rates"} + onChange={(v) => { + setBookingPanelState({ keepRatesPostBookingSubmit: v }); + }} + /> + </div> + </div> + </div> +</section> diff --git a/src/routes/user/components/data-entry-view.svelte b/src/routes/user/components/data-entry-view.svelte new file mode 100755 index 0000000..ce755d3 --- /dev/null +++ b/src/routes/user/components/data-entry-view.svelte @@ -0,0 +1,145 @@ +<script lang="ts"> + // @ts-ignore + import VirtualList from "@sveltejs/svelte-virtual-list"; + import Button from "$lib/components/atoms/button.svelte"; + import Checkbox from "$lib/components/atoms/checkbox.svelte"; + import Input from "$lib/components/atoms/input.svelte"; + import CenteredSpinner from "$lib/components/molecules/centered-spinner.svelte"; + import { + bookingPanelData, + selectedEntriesMap, + } from "$lib/stores/booking.state"; + import clsx from "clsx"; + + const dispatcher = createEventDispatcher(); + + let affectingRowVisibility = false; + let globalFilter = ""; + const setGlobalFilter = (filter: string) => { + globalFilter = filter; + }; + + const columns = [ + { id: "number", header: "No" }, + { id: "first", header: "F" }, + { id: "second", header: "S" }, + ]; + + let timer: NodeJS.Timeout; + const handleEntrySearch = (e: any) => { + clearTimeout(timer); + timer = setTimeout(() => { + setGlobalFilter(e.target.value); + start = 0; + end = 20; + }, 300); + }; + + let baseTableHeaderStyle = + "p-1 px-4 sticky top-0 bg-sky-50 text-sky-700 text-end"; + let baseTableBodyCellStyle = "p-1 px-4 border text-end"; + let start: number | undefined, end: number | undefined; +</script> + +<section class="w-full flex flex-col relative"> + <div + class="my-2 flex w-full justify-between items-end gap-2 flex-col md:flex-row" + > + <div class="flex justify-end w-full md:max-w-xs"> + <Input + placeholder={"Search"} + fieldWidth={"full"} + onInput={handleEntrySearch} + /> + </div> + <div class="flex justify-between w-full md:max-w-xs gap-2"> + <Button + fullwidth={"yes"} + intent={"dangerInverted"} + text={"Delete"} + onClick={() => dispatcher("deleteSelectedEntries")} + /> + <Button + fullwidth={"yes"} + intent={"primaryInverted"} + text={"Refresh"} + onClick={() => dispatcher("refreshData")} + /> + </div> + </div> + {#if affectingRowVisibility} + <CenteredSpinner /> + {:else} + <tr + class="cursor-pointer bg-sky-50 border-t border-b border-sky-500 transition-colors duration-100 grid grid-cols-7 w-full pr-4" + > + <th class={clsx(baseTableHeaderStyle, "col-span-1 justify-center flex")}> + <Checkbox + checked={"indeterminate"} + onChange={(v) => { + selectedEntriesMap.update((old) => { + for (const key of Object.keys(old)) { + old[key] = v === "indeterminate" ? false : v; + } + return old; + }); + }} + /> + </th> + {#each columns as col} + <th class={clsx(baseTableHeaderStyle, "col-span-2")}> + {col.header} + </th> + {/each} + </tr> + {#if $bookingPanelData.length > 0} + <div class="h-[72vh]"> + <VirtualList bind:end bind:start items={$bookingPanelData} let:item> + <tr + class="cursor-pointer hover:bg-slate-50 transition-colors duration-100 grid grid-cols-7 w-full" + > + <td + class={clsx( + baseTableBodyCellStyle, + "col-span-1 justify-center flex" + )} + > + {#if $selectedEntriesMap[item.id]} + <Checkbox + bind:checked={$selectedEntriesMap[item.id]} + onChange={(v) => { + $selectedEntriesMap[item.id] = + typeof v === "boolean" ? v : false; + }} + /> + {:else} + <Checkbox + bind:checked={$selectedEntriesMap[item.id]} + onChange={(v) => { + $selectedEntriesMap[item.id] = + typeof v === "boolean" ? v : false; + }} + /> + {/if} + </td> + <td class={clsx(baseTableBodyCellStyle, "col-span-2")}> + {item.number} + </td> + <td class={clsx(baseTableBodyCellStyle, "col-span-2")}> + {item.first} + </td> + <td class={clsx(baseTableBodyCellStyle, "col-span-2")}> + {item.second} + </td> + </tr> + </VirtualList> + </div> + {:else} + <span + class="w-full grid place-items-center pt-32 text-sky-400 tracking-wider font-bold text-xl" + > + <p>No data found</p> + </span> + {/if} + {/if} +</section> diff --git a/src/routes/user/components/draw-select.svelte b/src/routes/user/components/draw-select.svelte new file mode 100755 index 0000000..a95e4a6 --- /dev/null +++ b/src/routes/user/components/draw-select.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + import clsx from "clsx"; + import { randomString } from "$lib/utils"; + import type { Draw } from "$lib/utils/data.types"; + import { calculateTimeRemaining } from "$lib/utils/datetime.helper.utils"; + import { setBookingPanelState } from "$lib/stores/booking.state"; + + type DrawOption = Draw & { timeRemaining?: string | undefined }; + + export let componentId: string = randomString(10); + export let draws: DrawOption[]; + export let fullWidth: boolean = false; + export let monoFont: boolean = false; + + const updateChosenDraw = (e: Event) => { + const id = (e.target as HTMLSelectElement).value; + const draw = draws.find((draw) => draw.id === id); + if (draw) { + setBookingPanelState({ chosenDraw: draw }); + } + }; + + setBookingPanelState({ chosenDraw: draws[0] }); + + onMount(() => { + const tickingInterval = setInterval(() => { + for (let i = 0; i < draws.length; i++) { + draws[i].timeRemaining = calculateTimeRemaining( + draws[i].closeTime, + true + ); + } + draws = draws; + }, 1000); + return () => clearInterval(tickingInterval); + }); +</script> + +<div class={clsx("relative flex flex-col gap-1", fullWidth && "w-full")}> + <select + id={componentId} + on:change={updateChosenDraw} + class={clsx( + "rounded-md relative cursor-pointer border text-left border-gray-300 outline-none p-2 transition-colors duration-200 bg-slate-50 hover:bg-slate-50 hover:border-black focus:border-sky-500 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700", + fullWidth ? "w-full" : "w-max", + monoFont ? "font-mono" : "" + )} + > + {#each draws as option} + <option class={"bg-white text-black"} value={option.id} + >{option.timeRemaining} - {option.title}</option + > + {/each} + </select> +</div> diff --git a/src/routes/user/components/live-clock.svelte b/src/routes/user/components/live-clock.svelte new file mode 100755 index 0000000..8ac0e53 --- /dev/null +++ b/src/routes/user/components/live-clock.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + import IcBaselineAccessTime from "~icons/ic/baseline-access-time"; + export let startingTime: Date | undefined = undefined; + export let label: string; + + $: _time = startingTime; + $: getTimings = () => { + return [ + _time ? _time.getFullYear().toString().slice(0, 2) : "----", + _time ? (_time.getMonth() + 1).toString().padStart(2, "0") : "--", + _time ? _time.getDate().toString().padStart(2, "0") : "--", + _time ? _time.getHours().toString().padStart(2, "0") : "--", + _time ? _time.getMinutes().toString().padStart(2, "0") : "--", + _time ? _time.getSeconds().toString().padStart(2, "0") : "--", + ]; + }; + $: [years, months, days, hours, minutes, seconds] = getTimings(); + + onMount(() => { + const clockTerval = setInterval(() => { + if (!_time) return; + _time = new Date(new Date(_time).getTime() + 1000); + }, 1000); + return () => clearInterval(clockTerval); + }); +</script> + +<div + class="p-2 px-4 border shadow-sm border-sky-400 rounded-md bg-sky-50 text-sky-800 items-center font-mono flex gap-2" +> + <span>{label}</span> + <span>-</span> + <span>{years}</span> + <span>/</span> + <span>{months}</span> + <span>/</span> + <span>{days}</span> + <IcBaselineAccessTime class="w-5 h-5 mb-0.5" /> + <span>{hours}</span> + <span>:</span> + <span>{minutes}</span> + <span>:</span> + <span>{seconds}</span> +</div> diff --git a/src/routes/user/components/schemes-section.svelte b/src/routes/user/components/schemes-section.svelte new file mode 100755 index 0000000..7cf76ff --- /dev/null +++ b/src/routes/user/components/schemes-section.svelte @@ -0,0 +1,90 @@ +<script lang="ts"> + import IconTablerSquareRoundedChevronDown from "~icons/tabler/square-rounded-chevron-down"; + import Title from "$lib/components/atoms/title.svelte"; + import { COLOR_TRANSITION } from "$lib/utils/constants"; + import { + bookingPanelState, + setBookingPanelState, + } from "$lib/stores/booking.state"; + import clsx from "clsx"; + + export let title: string; + export let isPermutationMode: boolean; + export let schemes: Record<string, string[]>; + export let highLightIfPermutationMode: boolean; + + $: chosenScheme = $bookingPanelState.chosenScheme; + $: isPermutationModeSelected = $bookingPanelState.isPermutationModeSelected; + + const subSectionVisibility = {} as Record<string, boolean>; + for (const each of Object.keys(schemes)) { + subSectionVisibility[each] = false; + } +</script> + +<section + class="flex gap-4 p-4 rounded-lg shadow-sm border w-full flex-col h-full overflow-y-auto" +> + <Title text={title} size={"h4"} /> + {#each Object.entries(schemes) as [subTitle, schemesList]} + <div class="flex flex-col w-full"> + <button + class={clsx( + "w-full p-2 rounded-t-md bg-slate-100 text-slate-800 capitalize flex justify-between items-center hover:bg-slate-200 cursor-pointer", + COLOR_TRANSITION + )} + on:click={() => { + for (const each of Object.keys(subSectionVisibility)) { + if (each === subTitle) continue; + subSectionVisibility[each] = false; + } + subSectionVisibility[subTitle] = !subSectionVisibility[subTitle]; + }} + > + {#if !isPermutationMode} + {subTitle} + {/if} + <IconTablerSquareRoundedChevronDown + class={clsx( + "w-5 h-5", + subSectionVisibility[subTitle] ? "rotate-180" : "" + )} + /> + {#if isPermutationMode} + {subTitle} + {/if} + </button> + + <div + class={clsx( + "w-full pt-2 flex flex-col gap-2 cursor-pointer", + COLOR_TRANSITION, + subSectionVisibility[subTitle] ? "block" : "hidden" + )} + > + {#each schemesList as scheme} + <button + class={clsx( + "w-full p-2 rounded-md cursor-pointer font-mono outline-1 outline-sky-500 uppercase text-sm tracking-wide", + COLOR_TRANSITION, + isPermutationMode ? "text-end" : "text-start", + chosenScheme === scheme && + ((isPermutationModeSelected && highLightIfPermutationMode) || + (!isPermutationModeSelected && !highLightIfPermutationMode)) + ? "bg-sky-500 text-white hover:bg-sky-600" + : "bg-sky-50 text-sky-500 hover:bg-sky-100" + )} + on:click={() => { + setBookingPanelState({ + chosenScheme: scheme, + isPermutationModeSelected: isPermutationMode, + }); + }} + > + {scheme} + </button> + {/each} + </div> + </div> + {/each} +</section> diff --git a/src/routes/user/page.types.ts b/src/routes/user/page.types.ts new file mode 100755 index 0000000..e69de29 diff --git a/src/routes/user/user-navbar.svelte b/src/routes/user/user-navbar.svelte new file mode 100755 index 0000000..4d23e6c --- /dev/null +++ b/src/routes/user/user-navbar.svelte @@ -0,0 +1,57 @@ +<script lang="ts"> + import IconSignOut from "~icons/ph/sign-out"; + import { goto } from "$app/navigation"; + import LinkButton from "$lib/components/atoms/link-button.svelte"; + import type { trpc } from "$lib/trpc/trpc"; + + import DrawSelect from "./components/draw-select.svelte"; + import LiveClock from "./components/live-clock.svelte"; + + export let username: string = ""; + export let api: ReturnType<typeof trpc>; + + let myLocalTime: Date | undefined = undefined; + + let panelData = api.booking.getPanelData.createQuery(undefined, { + refetchOnWindowFocus: false, + refetchInterval: false, + }); + + $: draws = $panelData.data ? $panelData.data.draws : []; + + const logOut = async () => { + await fetch("/api/auth/logout", { method: "POST" }); + goto("/auth/signin"); + }; + + onMount(() => { + // parse the user's local time as an iso time string, in their local time zone + myLocalTime = new Date( + new Date().toLocaleString("en-US", { + timeZone: "Asia/Karachi", + timeZoneName: "short", + }) + ); + }); +</script> + +<nav class="flex w-full flex-col max-w-screen shadow-sm"> + <section class="flex w-full justify-between items-center p-4 md:px-8 pb-4"> + <div class="flex items-center gap-2"> + <span class="capitalize text-md md:text-lg lg:text-xl font-medium" + >{username}</span + > + </div> + <div class="w-full max-w-sm"> + {#if draws.length > 0} + <DrawSelect monoFont fullWidth {draws} /> + {/if} + </div> + {#if myLocalTime} + <LiveClock label={"L"} startingTime={myLocalTime} /> + {:else} + <LiveClock label={"L"} /> + {/if} + <LinkButton iconleft={IconSignOut} text={"Logout"} onClick={logOut} /> + </section> +</nav> diff --git a/static/favicon.png b/static/favicon.png new file mode 100755 index 0000000..d083070 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/fonts/InterV.ttf b/static/fonts/InterV.ttf new file mode 100755 index 0000000..ec3164e Binary files /dev/null and b/static/fonts/InterV.ttf differ diff --git a/static/images/signin-bg.svg b/static/images/signin-bg.svg new file mode 100755 index 0000000..cf9fcd7 --- /dev/null +++ b/static/images/signin-bg.svg @@ -0,0 +1,103 @@ +<svg width="1440" height="900" viewBox="0 0 1440 900" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_1707_14362)"> +<mask id="mask0_1707_14362" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="1440" height="900"> +<path d="M1440 0H0V900H1440V0Z" fill="white"/> +</mask> +<g mask="url(#mask0_1707_14362)"> +<path d="M1440 0H0V900H1440V0Z" fill="#075985"/> +<g style="mix-blend-mode:color-dodge"> +<path d="M141.358 246.312C215.112 119.681 321.259 96.3337 449.761 99.4994C559.715 102.206 678.61 131.562 780.09 88.2118C881.256 44.9931 951.272 -32.6756 1060.31 -28.21C1206.51 -22.2225 1269.48 96.6212 1260.63 213.287C1248.38 375.043 1111.35 457.44 975.438 448.706C862.958 441.481 743.869 431.287 643.404 506.249C550.869 575.29 487.949 717.627 359.976 730.618C107.484 756.249 4.94244 480.521 141.358 246.312Z" fill="url(#paint0_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:color-dodge"> +<path d="M1198.87 152.182C1141.67 94.0852 1048.06 94.0852 990.857 152.182L134.125 1022.3C76.9219 1080.4 76.9219 1175.47 134.125 1233.56C191.328 1291.66 284.934 1291.66 342.137 1233.56L1198.87 363.445C1256.08 305.348 1256.08 210.279 1198.87 152.182Z" fill="url(#paint1_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:color-dodge"> +<path d="M355.017 1010.77C390.642 1046.95 448.934 1046.95 484.558 1010.77L1018.1 468.897C1053.72 432.716 1053.72 373.512 1018.1 337.331C982.472 301.15 924.18 301.15 888.552 337.331L355.017 879.203C319.395 915.384 319.395 974.587 355.017 1010.77Z" fill="url(#paint2_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:color-dodge"> +<path d="M847.966 774.192C872.44 799.045 912.486 799.045 936.957 774.192L1303.48 401.941C1327.95 377.085 1327.95 336.413 1303.48 311.56C1279.01 286.704 1238.96 286.704 1214.49 311.56L847.966 683.81C823.496 708.663 823.496 749.335 847.966 774.192Z" fill="url(#paint3_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:screen"> +<path d="M946.453 353.643C946.063 353.643 945.669 353.49 945.364 353.187C944.764 352.577 944.764 351.587 945.364 350.977L1318.7 -28.1914C1319.3 -28.8008 1320.27 -28.8008 1320.87 -28.1914C1321.47 -27.582 1321.47 -26.5914 1320.87 -25.982L947.539 353.184C947.241 353.49 946.85 353.643 946.453 353.643Z" fill="url(#paint4_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:screen"> +<path d="M478.762 997.396C478.368 997.396 477.974 997.24 477.673 996.94C477.073 996.331 477.073 995.34 477.673 994.73L1129.98 332.23C1130.58 331.621 1131.56 331.621 1132.16 332.23C1132.76 332.84 1132.76 333.83 1132.16 334.44L479.848 996.94C479.55 997.24 479.156 997.396 478.762 997.396Z" fill="url(#paint5_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:screen"> +<path d="M21.1247 1027C20.8369 1027 20.5491 1026.89 20.3288 1026.67C19.8904 1026.22 19.8904 1025.5 20.3288 1025.05L326.082 714.334C326.52 713.889 327.233 713.889 327.671 714.334C328.11 714.78 328.11 715.504 327.671 715.949L21.9183 1026.67C21.7002 1026.89 21.4124 1027 21.1247 1027Z" fill="url(#paint6_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:screen"> +<path d="M1161.56 238.297C1161.52 238.297 1161.47 238.297 1161.42 238.29C1160.58 238.212 1159.95 237.45 1160.03 236.593L1162.68 207.093C1162.75 206.343 1163.34 205.747 1164.07 205.678L1191.83 203.115L1194.36 174.915C1194.43 174.165 1195.01 173.568 1195.75 173.5L1223.5 170.947L1226.03 142.75C1226.09 142 1226.68 141.403 1227.42 141.334L1256.45 138.665C1257.29 138.597 1258.04 139.222 1258.12 140.081C1258.19 140.94 1257.57 141.7 1256.72 141.778L1228.98 144.328L1226.45 172.525C1226.38 173.275 1225.79 173.872 1225.05 173.94L1197.31 176.493L1194.77 204.693C1194.71 205.443 1194.12 206.04 1193.38 206.109L1165.63 208.672L1163.09 236.875C1163.02 237.69 1162.35 238.297 1161.56 238.297Z" fill="url(#paint7_linear_1707_14362)"/> +</g> +<g style="mix-blend-mode:screen"> +<path d="M141.335 846.698C141.289 846.698 141.242 846.698 141.193 846.691C140.347 846.613 139.722 845.851 139.799 844.991L141.941 821.188C142.009 820.438 142.593 819.842 143.335 819.773L165.486 817.729L167.507 795.223C167.575 794.473 168.159 793.876 168.901 793.81L191.042 791.776L193.061 769.273C193.129 768.523 193.713 767.926 194.455 767.86L217.873 765.704C218.719 765.638 219.467 766.263 219.544 767.12C219.621 767.979 218.996 768.741 218.15 768.816L196.009 770.854L193.99 793.357C193.922 794.107 193.338 794.704 192.596 794.773L170.455 796.804L168.433 819.313C168.366 820.063 167.781 820.657 167.039 820.726L144.889 822.77L142.864 845.276C142.79 846.088 142.122 846.698 141.335 846.698Z" fill="url(#paint8_linear_1707_14362)"/> +</g> +</g> +</g> +<defs> +<linearGradient id="paint0_linear_1707_14362" x1="526.132" y1="79.1813" x2="882.91" y2="665.013" gradientUnits="userSpaceOnUse"> +<stop/> +<stop offset="0.2295" stop-color="#050505"/> +<stop offset="0.4926" stop-color="#131313"/> +<stop offset="0.7711" stop-color="#292929"/> +<stop offset="1" stop-color="#424242"/> +</linearGradient> +<linearGradient id="paint1_linear_1707_14362" x1="222.819" y1="1071.31" x2="1389.32" y2="106.719" gradientUnits="userSpaceOnUse"> +<stop/> +<stop offset="0.1942" stop-color="#050505"/> +<stop offset="0.4169" stop-color="#131313"/> +<stop offset="0.6536" stop-color="#2A2A2A"/> +<stop offset="0.8985" stop-color="#494949"/> +<stop offset="1" stop-color="#595959"/> +</linearGradient> +<linearGradient id="paint2_linear_1707_14362" x1="962.861" y1="438.379" x2="236.413" y2="1039.08" gradientUnits="userSpaceOnUse"> +<stop/> +<stop offset="0.1942" stop-color="#050505"/> +<stop offset="0.4169" stop-color="#131313"/> +<stop offset="0.6536" stop-color="#2A2A2A"/> +<stop offset="0.8985" stop-color="#494949"/> +<stop offset="1" stop-color="#595959"/> +</linearGradient> +<linearGradient id="paint3_linear_1707_14362" x1="1265.53" y1="380.976" x2="766.485" y2="793.641" gradientUnits="userSpaceOnUse"> +<stop/> +<stop offset="0.1942" stop-color="#050505"/> +<stop offset="0.4169" stop-color="#131313"/> +<stop offset="0.6536" stop-color="#2A2A2A"/> +<stop offset="0.8985" stop-color="#494949"/> +<stop offset="1" stop-color="#595959"/> +</linearGradient> +<linearGradient id="paint4_linear_1707_14362" x1="944.914" y1="162.496" x2="1321.33" y2="162.496" gradientUnits="userSpaceOnUse"> +<stop stop-color="#918D60"/> +<stop offset="0.3772" stop-color="#555338"/> +<stop offset="0.799" stop-color="#181710"/> +<stop offset="1"/> +</linearGradient> +<linearGradient id="paint5_linear_1707_14362" x1="477.224" y1="664.584" x2="1132.61" y2="664.584" gradientUnits="userSpaceOnUse"> +<stop stop-color="#918D60"/> +<stop offset="0.3772" stop-color="#555338"/> +<stop offset="0.799" stop-color="#181710"/> +<stop offset="1"/> +</linearGradient> +<linearGradient id="paint6_linear_1707_14362" x1="328.001" y1="870.499" x2="19.9997" y2="870.499" gradientUnits="userSpaceOnUse"> +<stop stop-color="#918D60"/> +<stop offset="0.3772" stop-color="#555338"/> +<stop offset="0.799" stop-color="#181710"/> +<stop offset="1"/> +</linearGradient> +<linearGradient id="paint7_linear_1707_14362" x1="1160.02" y1="188.482" x2="1258.12" y2="188.482" gradientUnits="userSpaceOnUse"> +<stop stop-color="#918D60"/> +<stop offset="0.3772" stop-color="#555338"/> +<stop offset="0.799" stop-color="#181710"/> +<stop offset="1"/> +</linearGradient> +<linearGradient id="paint8_linear_1707_14362" x1="139.794" y1="806.196" x2="219.552" y2="806.196" gradientUnits="userSpaceOnUse"> +<stop stop-color="#918D60"/> +<stop offset="0.3772" stop-color="#555338"/> +<stop offset="0.799" stop-color="#181710"/> +<stop offset="1"/> +</linearGradient> +<clipPath id="clip0_1707_14362"> +<rect width="1440" height="900" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/svelte.config.js b/svelte.config.js new file mode 100755 index 0000000..7074a19 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,17 @@ +import adapter from "@sveltejs/adapter-node"; +import { vitePreprocess } from "@sveltejs/kit/vite"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter(), + }, +}; + +export default config; diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..7667e41 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,64 @@ +import { fontFamily } from "tailwindcss/defaultTheme"; +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: ["./src/**/*.{html,js,svelte,ts}"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + xs: "375px", + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border) / <alpha-value>)", + input: "hsl(var(--input) / <alpha-value>)", + ring: "hsl(var(--ring) / <alpha-value>)", + background: "hsl(var(--background) / <alpha-value>)", + foreground: "hsl(var(--foreground) / <alpha-value>)", + primary: { + DEFAULT: "hsl(var(--primary) / <alpha-value>)", + foreground: "hsl(var(--primary-foreground) / <alpha-value>)", + }, + secondary: { + DEFAULT: "hsl(var(--secondary) / <alpha-value>)", + foreground: "hsl(var(--secondary-foreground) / <alpha-value>)", + }, + destructive: { + DEFAULT: "hsl(var(--destructive) / <alpha-value>)", + foreground: "hsl(var(--destructive-foreground) / <alpha-value>)", + }, + muted: { + DEFAULT: "hsl(var(--muted) / <alpha-value>)", + foreground: "hsl(var(--muted-foreground) / <alpha-value>)", + }, + accent: { + DEFAULT: "hsl(var(--accent) / <alpha-value>)", + foreground: "hsl(var(--accent-foreground) / <alpha-value>)", + }, + popover: { + DEFAULT: "hsl(var(--popover) / <alpha-value>)", + foreground: "hsl(var(--popover-foreground) / <alpha-value>)", + }, + card: { + DEFAULT: "hsl(var(--card) / <alpha-value>)", + foreground: "hsl(var(--card-foreground) / <alpha-value>)", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: [...fontFamily.sans], + }, + }, + }, +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..6ae0c8c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100755 index 0000000..34d3f85 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,26 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import AutoImport from "unplugin-auto-import/vite"; +import IconsResolver from "unplugin-icons/resolver"; +import Icons from "unplugin-icons/vite"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + sveltekit(), + Icons({ compiler: "svelte" }), + AutoImport({ + resolvers: [ + IconsResolver({ + prefix: "Icon", + extension: "svelte", + }), + ], + dts: "src/auto-imports.d.ts", + imports: ["svelte"], + dirs: ["src"], + ignore: ["**/*.test.{js,ts}", "**/*.spec.{js,ts}"], + exclude: [/node_modules/, /@sveltejs\/kit/, /.git/], + }), + ], + test: { include: ["src/**/*.{test,spec}.{js,ts}"] }, +});