diff --git a/.gitignore b/.gitignore index 096a8b1..b8b77b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .DS_Store node_modules/ +dist/ project.private.config.json - diff --git a/Dockerfile b/Dockerfile index 5572499..c2f76b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,13 @@ +FROM node:22-alpine AS web-build +WORKDIR /app +COPY package.json package-lock.json vite.config.js ./ +RUN npm ci +COPY web ./web +RUN npm run build:web + FROM nginx:1.27-alpine -COPY web /usr/share/nginx/html +COPY --from=web-build /app/dist /usr/share/nginx/html +COPY assets /usr/share/nginx/html/assets COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 8080 HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -q -O- http://127.0.0.1:8080/health || exit 1 diff --git a/app.json b/app.json index 7a1b568..bc1697a 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,7 @@ { "pages": [ "pages/index/index", + "pages/discover/discover", "pages/test/test", "pages/result/result", "pages/me/me" @@ -15,4 +16,3 @@ "request": 10000 } } - diff --git a/assets/test-covers/bbq.jpg b/assets/test-covers/bbq.jpg new file mode 100644 index 0000000..19c61a5 Binary files /dev/null and b/assets/test-covers/bbq.jpg differ diff --git a/assets/test-covers/drink.jpg b/assets/test-covers/drink.jpg new file mode 100644 index 0000000..8c6248d Binary files /dev/null and b/assets/test-covers/drink.jpg differ diff --git a/assets/test-covers/dumpling.jpg b/assets/test-covers/dumpling.jpg new file mode 100644 index 0000000..0464f76 Binary files /dev/null and b/assets/test-covers/dumpling.jpg differ diff --git a/assets/test-covers/hotpot.jpg b/assets/test-covers/hotpot.jpg new file mode 100644 index 0000000..8ed1175 Binary files /dev/null and b/assets/test-covers/hotpot.jpg differ diff --git a/assets/test-covers/milk-tea.jpg b/assets/test-covers/milk-tea.jpg new file mode 100644 index 0000000..8c1c49b Binary files /dev/null and b/assets/test-covers/milk-tea.jpg differ diff --git a/assets/test-covers/noodle.jpg b/assets/test-covers/noodle.jpg new file mode 100644 index 0000000..d2ad363 Binary files /dev/null and b/assets/test-covers/noodle.jpg differ diff --git a/assets/test-covers/rice.jpg b/assets/test-covers/rice.jpg new file mode 100644 index 0000000..d6d2db3 Binary files /dev/null and b/assets/test-covers/rice.jpg differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c55086a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1199 @@ +{ + "name": "pk-lab-xhs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pk-lab-xhs", + "version": "0.1.0", + "dependencies": { + "motion": "^12.23.24" + }, + "devDependencies": { + "vite": "^7.1.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz", + "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz", + "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz", + "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz", + "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz", + "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz", + "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz", + "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz", + "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz", + "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz", + "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz", + "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz", + "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz", + "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz", + "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz", + "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz", + "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz", + "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz", + "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz", + "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz", + "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz", + "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz", + "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz", + "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz", + "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz", + "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/framer-motion": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.43.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/motion": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.43.0.tgz", + "integrity": "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.43.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz", + "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.3", + "@rollup/rollup-android-arm64": "4.63.3", + "@rollup/rollup-darwin-arm64": "4.63.3", + "@rollup/rollup-darwin-x64": "4.63.3", + "@rollup/rollup-freebsd-arm64": "4.63.3", + "@rollup/rollup-freebsd-x64": "4.63.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", + "@rollup/rollup-linux-arm-musleabihf": "4.63.3", + "@rollup/rollup-linux-arm64-gnu": "4.63.3", + "@rollup/rollup-linux-arm64-musl": "4.63.3", + "@rollup/rollup-linux-loong64-gnu": "4.63.3", + "@rollup/rollup-linux-loong64-musl": "4.63.3", + "@rollup/rollup-linux-ppc64-gnu": "4.63.3", + "@rollup/rollup-linux-ppc64-musl": "4.63.3", + "@rollup/rollup-linux-riscv64-gnu": "4.63.3", + "@rollup/rollup-linux-riscv64-musl": "4.63.3", + "@rollup/rollup-linux-s390x-gnu": "4.63.3", + "@rollup/rollup-linux-x64-gnu": "4.63.3", + "@rollup/rollup-linux-x64-musl": "4.63.3", + "@rollup/rollup-openbsd-x64": "4.63.3", + "@rollup/rollup-openharmony-arm64": "4.63.3", + "@rollup/rollup-win32-arm64-msvc": "4.63.3", + "@rollup/rollup-win32-ia32-msvc": "4.63.3", + "@rollup/rollup-win32-x64-gnu": "4.63.3", + "@rollup/rollup-win32-x64-msvc": "4.63.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json index 9a165b1..b2f7f84 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,11 @@ "name": "pk-lab-xhs", "version": "0.1.0", "private": true, - "scripts": { "check": "node scripts/check.mjs" } + "scripts": { + "check": "node scripts/check.mjs", + "test": "node --test test/*.test.cjs", + "build:web": "vite build --config vite.config.js" + }, + "dependencies": { "motion": "^12.23.24" }, + "devDependencies": { "vite": "^7.1.7" } } - diff --git a/pages/discover/discover.css b/pages/discover/discover.css new file mode 100644 index 0000000..4b575c5 --- /dev/null +++ b/pages/discover/discover.css @@ -0,0 +1,18 @@ +.discover { padding: 28rpx 18rpx 180rpx; background: #fff; } +.heading { font-size: 46rpx; font-weight: 950; } +.subheading { margin: 10rpx 0 34rpx; color: #777; font-size: 24rpx; } +.result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; } +.result-card { overflow: hidden; border: 1rpx solid #eee; background: #fff; } +.result-card image { display: block; width: 100%; height: 290rpx; } +.result-copy { padding: 16rpx; } +.result-copy text { color: #777; font-size: 21rpx; } +.result-copy view { margin-top: 6rpx; font-size: 31rpx; font-weight: 900; } +.bottom-nav { position: fixed; z-index: 20; left: 28rpx; right: 28rpx; bottom: calc(24rpx + env(safe-area-inset-bottom)); display: flex; height: 104rpx; padding: 0 18rpx; border-radius: 20rpx; background: #080808; box-shadow: 0 18rpx 50rpx rgba(0,0,0,.22); } +.nav-item { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; font-size: 18rpx; } +.nav-item.active { color: #fff; } +.nav-item.active:before { content: ''; position: absolute; top: -25rpx; width: 94rpx; height: 94rpx; border: 14rpx solid #fff; border-radius: 50%; background: #080808; } +.nav-icon { z-index: 1; width: 26rpx; height: 26rpx; margin-bottom: 7rpx; border: 5rpx solid currentColor; } +.home-icon { transform: rotate(45deg); border-radius: 5rpx; } +.discover-icon { border-radius: 50%; border-width: 6rpx 2rpx; } +.me-icon { border-radius: 50%; } +.nav-item text { z-index: 1; } diff --git a/pages/discover/discover.js b/pages/discover/discover.js new file mode 100644 index 0000000..fec26ee --- /dev/null +++ b/pages/discover/discover.js @@ -0,0 +1,16 @@ +var api = require('../../utils/api'); + +Page({ + data: { results: [], loading: true, error: '' }, + onLoad: function () { this.load(); }, + load: function () { + var self = this; + api.request('/api/v1/results/discover').then(function (results) { + self.setData({ results: results, loading: false, error: '' }); + }).catch(function (error) { + self.setData({ loading: false, error: error.message || '加载失败' }); + }); + }, + openHome: function () { xhs.redirectTo({ url: '/pages/index/index' }); }, + openMe: function () { xhs.redirectTo({ url: '/pages/me/me' }); } +}); diff --git a/pages/discover/discover.json b/pages/discover/discover.json new file mode 100644 index 0000000..61b9721 --- /dev/null +++ b/pages/discover/discover.json @@ -0,0 +1 @@ +{ "navigationBarTitleText": "大家的选择" } diff --git a/pages/discover/discover.xhsml b/pages/discover/discover.xhsml new file mode 100644 index 0000000..0a21eca --- /dev/null +++ b/pages/discover/discover.xhsml @@ -0,0 +1,18 @@ + + 大家最近选了什么 + 看看同频的人把谁送到了最后 + 正在收集答案… + {{error}} + 还没有公开结果 + + + + {{item.test_title}}{{item.champion_name}} + + + + 测试 + 发现 + 我的 + + diff --git a/pages/index/index.css b/pages/index/index.css index c0be834..5a6981d 100644 --- a/pages/index/index.css +++ b/pages/index/index.css @@ -1,23 +1,28 @@ -.home { padding-bottom: 70rpx; } -.hero { position: relative; overflow: hidden; background: #ff6b35; color: #fff; border-radius: 36rpx; padding: 40rpx 34rpx 34rpx; box-shadow: 0 18rpx 50rpx rgba(168, 65, 25, .18); } -.hero-row { display: flex; justify-content: space-between; align-items: flex-start; } -.eyebrow { font-size: 21rpx; letter-spacing: 4rpx; opacity: .8; } -.title { width: 480rpx; margin-top: 18rpx; font-size: 58rpx; line-height: 1.05; font-weight: 900; } -.hero-copy { margin-top: 42rpx; width: 480rpx; font-size: 27rpx; line-height: 1.6; opacity: .92; } -.me-button { margin: 0; padding: 0 24rpx; min-width: 0; height: 64rpx; line-height: 64rpx; font-size: 25rpx; border-radius: 999rpx; background: rgba(255,255,255,.2); color: #fff; } -.hero-stickers { margin-top: 30rpx; display: flex; justify-content: space-between; font-size: 30rpx; font-weight: 800; } -.section-head { display: flex; justify-content: space-between; margin: 48rpx 6rpx 24rpx; font-size: 30rpx; font-weight: 800; } -.section-head .muted { font-size: 24rpx; font-weight: 500; } -.waterfall { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-gap: 22rpx; align-items: start; } -.test-card { overflow: hidden; background: #fff; border-radius: 28rpx; box-shadow: 0 9rpx 30rpx rgba(62, 42, 30, .08); } -.test-card:active { transform: scale(.98); } -.card-art { position: relative; height: 250rpx; overflow: hidden; } -.card-1 .card-art { height: 310rpx; } -.card-number { position: absolute; left: 20rpx; top: 18rpx; color: rgba(255,255,255,.8); font-size: 22rpx; font-weight: 800; } -.card-emoji { position: absolute; left: 50%; top: 52%; transform: translate(-50%, -50%) rotate(-8deg); font-size: 112rpx; } -.card-body { padding: 24rpx; } -.card-title { font-size: 30rpx; font-weight: 900; } -.card-copy { margin-top: 12rpx; color: #746a63; font-size: 23rpx; line-height: 1.45; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } -.card-meta { margin-top: 22rpx; display: flex; justify-content: space-between; color: #9a8e86; font-size: 20rpx; } +.home { padding: 22rpx 18rpx 180rpx; background: #fff; } +.mast { display: flex; align-items: center; gap: 16rpx; padding: 8rpx 4rpx 22rpx; } +.logo-mark { position: relative; display: grid; place-items: center; width: 72rpx; height: 58rpx; overflow: hidden; background: #111; color: #fff; font-size: 24rpx; font-weight: 950; letter-spacing: -2rpx; } +.logo-cut { position: absolute; left: 25rpx; top: -18rpx; width: 28rpx; height: 28rpx; border: 8rpx solid #fff; border-radius: 50%; } +.logo-word { font-size: 31rpx; font-weight: 950; letter-spacing: 2rpx; } +.search { display: flex; align-items: center; height: 72rpx; margin-bottom: 20rpx; padding: 0 24rpx; background: #f3f3f3; border-radius: 8rpx; } +.search-icon { margin-right: 14rpx; font-size: 38rpx; transform: rotate(-20deg); } +.search input { flex: 1; font-size: 27rpx; } +.waterfall { column-count: 2; column-gap: 10rpx; } +.test-card { display: inline-block; width: 100%; margin: 0 0 18rpx; overflow: hidden; break-inside: avoid; background: #fff; } +.test-card:active { opacity: .78; transform: scale(.985); } +.card-art { display: block; width: 100%; height: 360rpx; background: #eee; } +.card-1 .card-art { height: 430rpx; } +.card-2 .card-art { height: 390rpx; } +.card-body { padding: 16rpx 8rpx 10rpx; } +.card-title { font-size: 28rpx; font-weight: 850; } +.card-copy { margin-top: 8rpx; color: #555; font-size: 22rpx; line-height: 1.42; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } +.card-meta { margin-top: 12rpx; display: flex; justify-content: space-between; color: #999; font-size: 19rpx; } .retry { margin-top: 24rpx; width: 240rpx; font-size: 24rpx; } - +.bottom-nav { position: fixed; z-index: 20; left: 28rpx; right: 28rpx; bottom: calc(24rpx + env(safe-area-inset-bottom)); display: flex; height: 104rpx; padding: 0 18rpx; border-radius: 20rpx; background: #080808; box-shadow: 0 18rpx 50rpx rgba(0,0,0,.22); } +.nav-item { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; font-size: 18rpx; } +.nav-item.active { color: #fff; } +.nav-item.active:before { content: ''; position: absolute; top: -25rpx; width: 94rpx; height: 94rpx; border: 14rpx solid #fff; border-radius: 50%; background: #080808; } +.nav-icon { z-index: 1; width: 26rpx; height: 26rpx; margin-bottom: 7rpx; border: 5rpx solid currentColor; } +.home-icon { transform: rotate(45deg); border-radius: 5rpx; } +.discover-icon { border-radius: 50%; border-width: 6rpx 2rpx; } +.me-icon { border-radius: 50%; } +.nav-item text { z-index: 1; } diff --git a/pages/index/index.js b/pages/index/index.js index eb68a35..cf6908d 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -1,13 +1,13 @@ var api = require('../../utils/api'); Page({ - data: { tests: [], loading: true, error: '' }, + data: { tests: [], visibleTests: [], keyword: '', loading: true, error: '' }, onLoad: function () { this.loadTests(); }, onPullDownRefresh: function () { this.loadTests(true); }, loadTests: function (refreshing) { var self = this; api.request('/api/v1/tests').then(function (tests) { - self.setData({ tests: tests, loading: false, error: '' }); + self.setData({ tests: tests, visibleTests: tests, loading: false, error: '' }); }).catch(function (error) { self.setData({ loading: false, error: error.message || '加载失败' }); }).then(function () { @@ -19,5 +19,13 @@ Page({ openTest: function (event) { xhs.navigateTo({ url: '/pages/test/test?slug=' + event.currentTarget.dataset.slug }); }, + search: function (event) { + var keyword = (event.detail.value || '').trim().toLowerCase(); + var visible = this.data.tests.filter(function (item) { + return (item.title + ' ' + item.subtitle).toLowerCase().indexOf(keyword) >= 0; + }); + this.setData({ keyword: keyword, visibleTests: visible }); + }, + openDiscover: function () { xhs.navigateTo({ url: '/pages/discover/discover' }); }, openMe: function () { xhs.navigateTo({ url: '/pages/me/me' }); } }); diff --git a/pages/index/index.xhsml b/pages/index/index.xhsml index 6e0170c..7e6dc91 100644 --- a/pages/index/index.xhsml +++ b/pages/index/index.xhsml @@ -1,29 +1,21 @@ - - - - TASTE LAB · 01 - 今天,认真选一个。 - - - - 没有“都可以”。每轮只选一个,让你的偏爱一路赢到底。 - ✦二选一↗ - - 全部测试{{tests.length}} 个题库 + PK偏爱研究所 + ⌕ 正在摆好擂台… {{error}} - - - 0{{index + 1}}{{item.emoji}} - + + {{item.title}} {{item.subtitle}} - {{item.candidate_count}} 强{{item.run_count}} 人完成 → + {{item.candidate_count}} 强{{item.run_count}} 人完成 + + 测试 + 发现 + 我的 + - diff --git a/pages/me/me.css b/pages/me/me.css index 6f3b658..f335f27 100644 --- a/pages/me/me.css +++ b/pages/me/me.css @@ -13,4 +13,13 @@ .result-test { color: #887c74; font-size: 22rpx; } .result-champion { margin-top: 6rpx; font-size: 34rpx; font-weight: 900; } .arrow { font-size: 34rpx; } - +.profile { padding-bottom: 180rpx; } +.bottom-nav { position: fixed; z-index: 20; left: 28rpx; right: 28rpx; bottom: calc(24rpx + env(safe-area-inset-bottom)); display: flex; height: 104rpx; padding: 0 18rpx; border-radius: 20rpx; background: #080808; box-shadow: 0 18rpx 50rpx rgba(0,0,0,.22); } +.nav-item { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; font-size: 18rpx; } +.nav-item.active { color: #fff; } +.nav-item.active:before { content: ''; position: absolute; top: -25rpx; width: 94rpx; height: 94rpx; border: 14rpx solid #fff; border-radius: 50%; background: #080808; } +.nav-icon { z-index: 1; width: 26rpx; height: 26rpx; margin-bottom: 7rpx; border: 5rpx solid currentColor; } +.home-icon { transform: rotate(45deg); border-radius: 5rpx; } +.discover-icon { border-radius: 50%; border-width: 6rpx 2rpx; } +.me-icon { border-radius: 50%; } +.nav-item text { z-index: 1; } diff --git a/pages/me/me.js b/pages/me/me.js index e51156c..fe3b57b 100644 --- a/pages/me/me.js +++ b/pages/me/me.js @@ -19,6 +19,7 @@ Page({ api.request('/api/v1/me/link-code', { method: 'POST' }).then(function (data) { self.setData({ linkCode: data.code }); }).catch(function (error) { xhs.showToast({ title: error.message || '生成失败', icon: 'none' }); }); - } + }, + openHome: function () { xhs.redirectTo({ url: '/pages/index/index' }); }, + openDiscover: function () { xhs.redirectTo({ url: '/pages/discover/discover' }); } }); - diff --git a/pages/me/me.xhsml b/pages/me/me.xhsml index 25dc87c..e778886 100644 --- a/pages/me/me.xhsml +++ b/pages/me/me.xhsml @@ -15,5 +15,9 @@ #1{{item.test_title}}{{item.champion_name}}→ + + 测试 + 发现 + 我的 + - diff --git a/pages/result/result.css b/pages/result/result.css index d6a1664..bb44c96 100644 --- a/pages/result/result.css +++ b/pages/result/result.css @@ -1,16 +1,24 @@ -.result-kicker { text-align: center; color: #ff6b35; font-size: 22rpx; letter-spacing: 6rpx; font-weight: 800; } -.result-title { margin: 14rpx 0 28rpx; text-align: center; font-size: 46rpx; font-weight: 900; } -.champion { position: relative; overflow: hidden; padding: 54rpx 36rpx; text-align: center; border-radius: 36rpx; background: #25211f; color: #fff; } -.crown { position: absolute; right: 24rpx; top: 2rpx; color: #ffe568; font-size: 100rpx; transform: rotate(13deg); } -.test-name { opacity: .62; font-size: 24rpx; } -.champion-name { margin-top: 18rpx; font-size: 70rpx; line-height: 1.15; font-weight: 900; } -.runner { margin-top: 22rpx; color: #e5dcd6; font-size: 23rpx; } -.poster-head { display: flex; justify-content: space-between; margin: 46rpx 4rpx 20rpx; font-size: 30rpx; font-weight: 900; } -.poster-head .muted { font-size: 23rpx; font-weight: 500; } -.poster { width: 100%; height: 925rpx; border-radius: 30rpx; box-shadow: 0 16rpx 40rpx rgba(50,30,20,.12); } -.theme-scroll { margin-top: 26rpx; white-space: nowrap; } -.theme-row { display: flex; } -.theme-chip { flex: none; margin: 0 14rpx 0 0; padding: 0 28rpx; height: 68rpx; line-height: 68rpx; border-radius: 99rpx; background: #fff; color: #6d625b; font-size: 23rpx; } -.theme-chip.active { background: #25211f; color: #fff; } -.sticker-row { display: flex; justify-content: space-between; margin-top: 26rpx; padding: 26rpx; border-radius: 24rpx; background: #fff; font-size: 25rpx; } -.save, .again { margin-top: 22rpx; font-size: 28rpx; } +.editor { min-height: 100vh; padding: 20rpx 22rpx 40rpx; background: #151515; color: #fff; } +.editor-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18rpx; } +.editor-head text { font-size: 31rpx; font-weight: 900; } +.editor-head view view { margin-top: 4rpx; color: #888; font-size: 19rpx; } +.export { margin: 0; padding: 0 30rpx; min-width: 0; height: 64rpx; line-height: 64rpx; border-radius: 99rpx; background: #ff3156; color: #fff; font-size: 23rpx; font-weight: 800; } +.poster { width: 100%; height: 880rpx; background: #242424; } +.strip, .media-strip, .tool-strip { white-space: nowrap; } +.strip-row, .media-row, .tool-row { display: flex; align-items: center; } +.strip { margin-top: 18rpx; } +.chip { flex: none; margin-right: 10rpx; padding: 0 22rpx; min-width: 0; height: 54rpx; line-height: 54rpx; border-radius: 8rpx; background: #292929; color: #aaa; font-size: 20rpx; } +.chip.active { background: #fff; color: #111; } +.media-strip { margin-top: 22rpx; } +.media-thumb, .add-media { flex: none; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 112rpx; height: 112rpx; margin-right: 12rpx; border: 2rpx solid #444; border-radius: 10rpx; background: #252525; color: #fff; font-size: 20rpx; } +.bracket-thumb { border-color: #fff; background: #f3eadf; color: #111; font-weight: 800; } +.add-media text { margin-top: 5rpx; color: #999; font-size: 16rpx; } +.tool-strip { margin-top: 25rpx; } +.tool { flex: none; width: 104rpx; text-align: center; color: #888; font-size: 19rpx; } +.tool.active { color: #fff; } +.tool-icon { display: grid; place-items: center; width: 50rpx; height: 50rpx; margin: 0 auto 9rpx; border: 3rpx solid currentColor; border-radius: 12rpx; font-size: 18rpx; } +.tool-panel { display: flex; margin-top: 22rpx; padding: 18rpx; overflow: hidden; background: #222; } +.theme-chip { flex: 1; margin: 0 5rpx; padding: 0; height: 58rpx; line-height: 58rpx; border-radius: 6rpx; background: #333; color: #aaa; font-size: 19rpx; } +.theme-chip.active { background: #fff; color: #111; } +.setting-row { display: flex; width: 100%; justify-content: space-between; font-size: 22rpx; } +.hint { color: #777; font-size: 20rpx; } diff --git a/pages/result/result.js b/pages/result/result.js index cd78654..95282c9 100644 --- a/pages/result/result.js +++ b/pages/result/result.js @@ -1,4 +1,5 @@ var api = require('../../utils/api'); +var posterEditor = require('../../vendor/poster-editor.cjs'); var THEMES = { celebration: { bg: '#ff6b35', ink: '#fffaf4', card: '#25211f', accent: '#ffe568', label: '庆祝派对' }, @@ -16,9 +17,20 @@ Page({ { name: 'sticker', label: '贴纸手账' } ], sticker: true, + bracketLayout: 'symmetric', + bracketView: 'overview', + activeTool: 'filter', + backgroundPath: '', + tools: [ + { name: 'filter', label: '滤镜' }, { name: 'text', label: '文字' }, + { name: 'sticker', label: '贴纸' }, { name: 'crop', label: '裁剪' }, + { name: 'adjust', label: '调整' }, { name: 'layout', label: '布局' }, + { name: 'brush', label: '画笔' } + ], saving: false }, onLoad: function (options) { + this.posterDocument = posterEditor.createDocument({ id: 'run-' + options.id, seed: options.id, width: 750, height: 1000, meta: { sourceType: 'pk-run', sourceId: options.id } }); var cached = xhs.getStorageSync('pk_last_result'); this.runId = options.id; if (cached && cached.id === options.id) this.setData({ result: cached }); @@ -36,35 +48,45 @@ Page({ this.drawPoster(); }, toggleSticker: function () { this.setData({ sticker: !this.data.sticker }); this.drawPoster(); }, + pickBracketLayout: function (event) { this.setData({ bracketLayout: event.currentTarget.dataset.layout }); this.drawPoster(); }, + pickBracketView: function (event) { this.setData({ bracketView: event.currentTarget.dataset.view }); this.drawPoster(); }, + pickTool: function (event) { this.setData({ activeTool: event.currentTarget.dataset.tool }); }, + chooseBackground: function () { + var self = this; + xhs.chooseImage({ + count: 1, + success: function (result) { + var path = result.tempFilePaths && result.tempFilePaths[0]; + if (path) { self.setData({ backgroundPath: path }); self.drawPoster(); } + } + }); + }, drawPoster: function () { var result = this.data.result; if (!result) return; var theme = THEMES[this.data.template]; var isCelebration = this.data.template === 'celebration'; var showSticker = this.data.sticker; + var layout = this.data.bracketLayout; + var view = this.data.bracketView; + var backgroundPath = this.data.backgroundPath; xhs.createSelectorQuery().select('.poster').boundingClientRect(function (rect) { var ctx = xhs.createCanvasContext('poster'); if (rect) ctx.scale(rect.width / 750, rect.height / 1000); ctx.setFillStyle(theme.bg); ctx.fillRect(0, 0, 750, 1000); + if (backgroundPath) { + ctx.drawImage(backgroundPath, 0, 0, 750, 1000); + ctx.setGlobalAlpha(.82); ctx.setFillStyle(theme.bg); ctx.fillRect(0, 0, 750, 1000); ctx.setGlobalAlpha(1); + } ctx.setFillStyle(theme.ink); ctx.setFontSize(24); ctx.fillText('PK 研究所 / MY PICK', 54, 72); ctx.setFontSize(58); ctx.fillText('我的冠军是', 54, 156); - ctx.setFillStyle(theme.card); ctx.fillRect(46, 205, 658, 440); + ctx.setFillStyle(theme.card); ctx.fillRect(30, 190, 690, 610); ctx.setFillStyle(isCelebration ? '#fffaf4' : theme.ink); - ctx.setFontSize(26); ctx.fillText(result.test_title, 84, 270); - ctx.setFontSize(result.champion_name.length > 7 ? 62 : 82); - ctx.fillText(result.champion_name, 84, 405); - ctx.setFillStyle(theme.accent); ctx.fillRect(84, 440, Math.min(500, result.champion_name.length * 80), 18); - ctx.setFillStyle(isCelebration ? '#fffaf4' : theme.ink); - ctx.setFontSize(24); ctx.fillText('一路二选一,它赢到了最后。', 84, 525); - var ranking = result.ranking_names || []; - ctx.setFillStyle(theme.ink); ctx.setFontSize(25); ctx.fillText('我的 TOP 4', 54, 720); - for (var i = 0; i < Math.min(4, ranking.length); i += 1) { - ctx.setFontSize(26); ctx.fillText((i + 1) + '. ' + ranking[i], 64, 774 + i * 48); - } + ctx.setFontSize(22); ctx.fillText(result.test_title, 54, 235); + drawBracket(ctx, result, layout, view, theme, isCelebration); if (showSticker) { ctx.setFillStyle(theme.accent); ctx.beginPath(); ctx.arc(645, 125, 58, 0, Math.PI * 2); ctx.fill(); - ctx.setFillStyle(theme.card); ctx.setFontSize(28); ctx.fillText('WIN!', 607, 134); - ctx.setFontSize(52); ctx.fillText('✦', 640, 700); ctx.fillText('♡', 550, 900); + ctx.setFillStyle(theme.card); ctx.setFontSize(24); ctx.fillText('WIN', 615, 134); } ctx.setFillStyle(theme.ink); ctx.setFontSize(20); ctx.fillText('长按保存,把答案分享给同频的人', 54, 956); ctx.draw(); @@ -88,3 +110,53 @@ Page({ }, replay: function () { xhs.redirectTo({ url: '/pages/test/test?slug=' + this.data.result.test_slug }); } }); + +function drawBracket(ctx, result, layout, view, theme, isCelebration) { + var decisions = result.decisions || []; + var names = result.candidate_names || {}; + if (!decisions.length) { + ctx.setFillStyle(isCelebration ? '#fffaf4' : theme.ink); + ctx.setFontSize(72); + ctx.fillText(result.champion_name, 70, 480); + return; + } + var maxRound = decisions.reduce(function (max, item) { return Math.max(max, item.round); }, 1); + var minimumRound = view === 'final' ? maxRound : view === 'top8' ? Math.max(1, maxRound - 2) : view === 'top16' ? Math.max(1, maxRound - 3) : 1; + var visible = decisions.filter(function (item) { return item.round >= minimumRound; }); + if (view === 'left' || view === 'right') { + visible = visible.filter(function (item, index) { return item.round === maxRound || index % 2 === (view === 'left' ? 0 : 1); }); + } + var grouped = {}; + visible.forEach(function (item) { (grouped[item.round] || (grouped[item.round] = [])).push(item); }); + var positions = {}; + Object.keys(grouped).forEach(function (roundKey) { + var round = Number(roundKey), items = grouped[round], count = items.length; + items.forEach(function (item, index) { + var centered = layout === 'symmetric'; + var side = centered && round < maxRound && index % 2 ? 1 : -1; + var depth = maxRound - round; + var x = centered ? 375 + side * Math.min(250, 82 + depth * 68) : 64 + (round - minimumRound) * (590 / Math.max(1, maxRound - minimumRound)); + var y = 300 + (index + 1) * (440 / (count + 1)); + positions[item.winner_id + ':' + item.round] = { x: x, y: y, side: side, item: item }; + }); + }); + ctx.setLineWidth(3); + ctx.setStrokeStyle(theme.accent); + Object.keys(positions).forEach(function (key) { + var pos = positions[key], parent = null; + visible.forEach(function (candidate) { + if (!parent && candidate.round > pos.item.round && (candidate.left_id === pos.item.winner_id || candidate.right_id === pos.item.winner_id)) parent = positions[candidate.winner_id + ':' + candidate.round]; + }); + if (parent) { + ctx.beginPath(); ctx.moveTo(pos.x, pos.y); ctx.lineTo((pos.x + parent.x) / 2, pos.y); ctx.lineTo((pos.x + parent.x) / 2, parent.y); ctx.lineTo(parent.x, parent.y); ctx.stroke(); + } + }); + Object.keys(positions).forEach(function (key) { + var pos = positions[key], label = names[pos.item.winner_id] || result.champion_name; + ctx.setFillStyle(pos.item.round === maxRound ? theme.accent : theme.bg); + ctx.fillRect(pos.x - 58, pos.y - 21, 116, 42); + ctx.setFillStyle(theme.ink); ctx.setFontSize(label.length > 6 ? 14 : 17); + ctx.fillText(label.slice(0, 8), pos.x - 48, pos.y + 6); + }); + ctx.setFillStyle(theme.ink); ctx.setFontSize(24); ctx.fillText('冠军 ' + result.champion_name, 54, 850); +} diff --git a/pages/result/result.xhsml b/pages/result/result.xhsml index 19ab2e0..a20523b 100644 --- a/pages/result/result.xhsml +++ b/pages/result/result.xhsml @@ -1,15 +1,18 @@ - - FINAL ANSWER - 你的冠军诞生了! - ♛{{result.test_title}}{{result.champion_name}}决赛击败 {{result.runner_up_name}} - 分享卡片3 款模板任选 + + 分享图编辑器{{result.test_title}} · {{result.champion_name}} - - - - - - 添加手绘贴纸{{sticker ? '已开启 ✓' : '关闭'}} - - + + + + + + + + 晋级图 + + {{index + 1}}{{item.label}} + + + 装饰贴纸{{sticker ? '已开启' : '关闭'}} + 此工具将由 @entroia/poster-editor 的图层绑定驱动 diff --git a/pages/test/test.css b/pages/test/test.css index cc5808f..b053fcf 100644 --- a/pages/test/test.css +++ b/pages/test/test.css @@ -1,19 +1,28 @@ -.arena { min-height: 100vh; background: #fff8f2; } -.topline { display: flex; align-items: center; justify-content: space-between; } -.progress { margin-top: 20rpx; height: 10rpx; overflow: hidden; border-radius: 99rpx; background: #eee4dd; } -.progress-in { height: 100%; border-radius: inherit; background: #ff6b35; transition: width .25s; } -.question { margin: 54rpx 8rpx 34rpx; font-size: 52rpx; line-height: 1.18; font-weight: 900; } -.pair { position: relative; } -.choice { position: relative; min-height: 360rpx; padding: 34rpx; box-sizing: border-box; border-radius: 38rpx; color: #fff; box-shadow: 0 20rpx 50rpx rgba(80, 35, 18, .14); } -.choice + .choice { margin-top: 24rpx; } -.choice-a { background: #ff6b35; transform: rotate(-1deg); } -.choice-b { background: #2766df; transform: rotate(1deg); } -.choice:active { transform: scale(.975); } -.choice-index { width: 54rpx; height: 54rpx; line-height: 54rpx; text-align: center; border: 2rpx solid rgba(255,255,255,.6); border-radius: 50%; font-size: 24rpx; font-weight: 800; } -.food-mark { position: absolute; right: 34rpx; top: 24rpx; font-size: 92rpx; opacity: .8; transform: rotate(12deg); } -.choice-name { margin-top: 72rpx; font-size: 60rpx; line-height: 1.12; font-weight: 900; } -.tap-hint { margin-top: 54rpx; font-size: 24rpx; opacity: .75; } -.versus { position: absolute; z-index: 3; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(-8deg); width: 84rpx; height: 84rpx; line-height: 84rpx; text-align: center; color: #201d1b; background: #fff; border: 5rpx solid #201d1b; border-radius: 50%; font-size: 26rpx; font-weight: 900; } -.round-note { margin: 38rpx 0; text-align: center; color: #8f827a; font-size: 24rpx; } -.saving { position: fixed; left: 50%; bottom: 70rpx; transform: translateX(-50%); padding: 22rpx 34rpx; border-radius: 99rpx; background: #201d1b; color: #fff; font-size: 25rpx; } - +.arena { height: 100vh; overflow: hidden; padding: 24rpx 28rpx; background: #fff; } +.topline { display: flex; align-items: center; justify-content: space-between; font-size: 23rpx; font-weight: 800; } +.progress { margin-top: 14rpx; height: 7rpx; overflow: hidden; background: #e8e8e8; } +.progress-in { height: 100%; background: #111; transition: width .25s; } +.mode-tabs { display: flex; width: 430rpx; height: 62rpx; margin: 24rpx auto; padding: 5rpx; border-radius: 10rpx; background: #f1f1f1; } +.mode { flex: 1; display: grid; place-items: center; border-radius: 7rpx; color: #777; font-size: 23rpx; } +.mode.active { background: #111; color: #fff; font-weight: 800; } +.single-stage { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; height: calc(100vh - 190rpx); max-height: 850rpx; } +.choice { position: relative; display: flex; flex-direction: column; justify-content: center; min-width: 0; padding: 28rpx; color: #fff; transition: transform .2s cubic-bezier(.2,.9,.2,1), opacity .2s; } +.choice:active { transform: scale(.94) rotate(-1deg); opacity: .8; } +.choice-a { background: #f34b30; } +.choice-b { background: #3564dd; } +.choice-index { position: absolute; left: 24rpx; top: 24rpx; width: 48rpx; height: 48rpx; line-height: 48rpx; text-align: center; border: 2rpx solid rgba(255,255,255,.6); border-radius: 50%; font-size: 21rpx; } +.choice-name { font-size: 45rpx; line-height: 1.1; font-weight: 950; word-break: break-all; } +.tap-hint { position: absolute; left: 28rpx; bottom: 28rpx; font-size: 21rpx; opacity: .75; } +.versus { position: absolute; z-index: 3; left: 50%; top: 50%; transform: translate(-50%, -50%); display: grid; place-items: center; width: 66rpx; height: 66rpx; border: 5rpx solid #111; border-radius: 50%; background: #fff; font-size: 21rpx; font-weight: 950; } +.swipe-stage { perspective: 1000rpx; } +.swipe-stage .choice-a { transform: rotate(-2deg); transform-origin: bottom right; } +.swipe-stage .choice-b { transform: rotate(2deg); transform-origin: bottom left; } +.multi-stage { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 12rpx; height: calc(100vh - 190rpx); max-height: 850rpx; } +.mini-match { position: relative; display: grid; grid-template-columns: 1fr 42rpx 1fr; align-items: stretch; min-height: 0; overflow: hidden; border: 2rpx solid #111; } +.mini-choice { display: grid; place-items: center; padding: 16rpx; text-align: center; font-size: 27rpx; font-weight: 850; background: #fff; transition: .18s; } +.mini-choice:active { color: #fff; background: #111; transform: scale(.95); } +.mini-vs { display: grid; place-items: center; background: #111; color: #fff; font-size: 17rpx; font-weight: 900; } +.mini-winner { grid-column: 1 / 4; display: flex; flex-direction: column; align-items: center; justify-content: center; animation: winner-in .35s cubic-bezier(.2,1.3,.4,1); background: #111; color: #fff; font-size: 34rpx; font-weight: 950; } +.mini-winner text { margin-bottom: 10rpx; color: #d9ff54; font-size: 18rpx; letter-spacing: 4rpx; } +.saving { position: fixed; z-index: 10; left: 50%; bottom: 50rpx; transform: translateX(-50%); padding: 20rpx 30rpx; border-radius: 99rpx; background: #111; color: #fff; font-size: 23rpx; } +@keyframes winner-in { from { opacity: 0; transform: scale(.65) rotate(-4deg); } to { opacity: 1; transform: scale(1); } } diff --git a/pages/test/test.js b/pages/test/test.js index d68998b..07f1dfd 100644 --- a/pages/test/test.js +++ b/pages/test/test.js @@ -2,7 +2,7 @@ var api = require('../../utils/api'); var bracket = require('../../utils/bracket'); Page({ - data: { loading: true, error: '', test: null, left: null, right: null, round: 1, progress: 0, completed: 0, totalDecisions: 0 }, + data: { loading: true, error: '', test: null, left: null, right: null, matches: [], mode: 'single', round: 1, progress: 0, completed: 0, totalDecisions: 0 }, onLoad: function (options) { this.slug = options.slug; this.load(); @@ -19,14 +19,26 @@ Page({ }, refreshPair: function () { var pair = bracket.current(this.state); + if (this.visibleRound !== this.state.round) { + this.visibleRound = this.state.round; + this.multiOffset = 0; + } + var offset = this.multiOffset || 0; + var matches = this.state.matches.slice(offset, offset + 4).map(function (match, index) { + return { index: offset + index, left: match.left, right: match.right, winner: match.winner }; + }); this.setData({ left: pair && pair[0], right: pair && pair[1], + matches: matches, round: this.state.round, completed: this.state.decisions.length, progress: Math.round(this.state.decisions.length / this.totalDecisions * 100) }); }, + setMode: function (event) { + this.setData({ mode: event.currentTarget.dataset.mode }); + }, choose: function (event) { if (this.choosing) return; this.choosing = true; @@ -36,9 +48,43 @@ Page({ if (finished) this.complete(); else { this.refreshPair(); - this.choosing = false; + var self = this; + setTimeout(function () { self.choosing = false; }, 220); } }, + chooseMatch: function (event) { + if (this.choosing) return; + var matchIndex = Number(event.currentTarget.dataset.match); + var side = event.currentTarget.dataset.side; + var match = this.state.matches[matchIndex]; + if (!match || match.winner || !match.right) return; + this.choosing = true; + var winner = side === 'left' ? match.left : match.right; + var beforeRound = this.state.round; + var finished = bracket.chooseAt(this.state, matchIndex, winner); + if (finished) this.complete(); + else { + this.refreshPair(); + var self = this; + setTimeout(function () { + if (beforeRound === self.state.round) { + var offset = self.multiOffset || 0; + var block = self.state.matches.slice(offset, offset + 4); + if (block.length && block.every(function (item) { return item.winner; })) self.multiOffset = offset + 4; + } + self.refreshPair(); + self.choosing = false; + }, 360); + } + }, + touchStart: function (event) { this.touchX = event.touches[0].clientX; }, + touchEnd: function (event) { + if (this.data.mode !== 'swipe' || this.touchX == null) return; + var delta = event.changedTouches[0].clientX - this.touchX; + this.touchX = null; + if (Math.abs(delta) < 55) return; + this.choose({ currentTarget: { dataset: { side: delta < 0 ? 'left' : 'right' } } }); + }, complete: function () { var self = this; var champion = this.state.pool[0]; diff --git a/pages/test/test.xhsml b/pages/test/test.xhsml index 138dfd5..9c19a9a 100644 --- a/pages/test/test.xhsml +++ b/pages/test/test.xhsml @@ -2,19 +2,28 @@ 正在抽签… {{error}} - 第 {{round}} 轮{{progress}}% + 第 {{round}} 轮{{completed}} / {{totalDecisions}} - 此刻只能留一个,你会选谁? - - - A{{test.emoji}}{{left.name}}点它晋级 ↗ - + + 一对 + 多对 + 滑动 + + + A{{left.name}}{{mode === 'swipe' ? '向左滑选它' : '点按晋级'}} VS - - B{{test.emoji}}{{right.name}}点它晋级 ↗ + B{{right.name}}{{mode === 'swipe' ? '向右滑选它' : '点按晋级'}} + + + + 晋级{{item.winner.name}} + + {{item.left.name}} + VS + {{item.right.name}} + - 已完成 {{completed}} / {{totalDecisions}} 次选择 正在保存你的冠军… diff --git a/scripts/check.mjs b/scripts/check.mjs index e9a9661..6230178 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -1,8 +1,7 @@ import { readFileSync, existsSync } from 'node:fs'; -const required = ['app.js','app.json','pages/index/index.xhsml','pages/test/test.js','pages/result/result.js','web/index.html','web/app.js']; +const required = ['app.js','app.json','pages/index/index.xhsml','pages/discover/discover.xhsml','pages/test/test.js','pages/result/result.js','assets/test-covers/rice.jpg','vendor/poster-editor.cjs','web/index.html','web/app.js']; for (const file of required) if (!existsSync(file)) throw new Error(`Missing ${file}`); JSON.parse(readFileSync('app.json','utf8')); const web = readFileSync('web/index.html','utf8'); if (!web.includes('./app.js') || !web.includes('./style.css')) throw new Error('Preview assets are not linked'); console.log(`Static checks passed (${required.length} required files).`); - diff --git a/scripts/check.py b/scripts/check.py index d304e32..d42db7b 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -5,8 +5,11 @@ required = [ "app.js", "app.json", "pages/index/index.xhsml", + "pages/discover/discover.xhsml", "pages/test/test.js", "pages/result/result.js", + "assets/test-covers/rice.jpg", + "vendor/poster-editor.cjs", "web/index.html", "web/app.js", ] diff --git a/test/bracket.test.cjs b/test/bracket.test.cjs new file mode 100644 index 0000000..ea65209 --- /dev/null +++ b/test/bracket.test.cjs @@ -0,0 +1,25 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const bracket = require('../utils/bracket'); + +function candidates(count) { + return Array.from({ length: count }, (_, index) => ({ id: String(index), name: `C${index}` })); +} + +test('supports selecting several matches in the same round', () => { + const state = bracket.create(candidates(4)); + assert.equal(bracket.chooseAt(state, 1, state.matches[1].right), false); + assert.equal(state.round, 1); + assert.equal(bracket.chooseAt(state, 0, state.matches[0].left), false); + assert.equal(state.round, 2); + assert.equal(bracket.choose(state, state.matches[0].left), true); + assert.equal(state.pool.length, 1); + assert.equal(state.decisions.length, 3); +}); + +test('odd candidates receive a deterministic bye', () => { + const state = bracket.create(candidates(3)); + bracket.chooseAt(state, 0, state.matches[0].left); + assert.equal(state.round, 2); + assert.equal(state.matches[0].right.id, '2'); +}); diff --git a/utils/bracket.js b/utils/bracket.js index 68be2fa..2993228 100644 --- a/utils/bracket.js +++ b/utils/bracket.js @@ -1,44 +1,48 @@ +function buildMatches(pool) { + var matches = []; + for (var index = 0; index < pool.length; index += 2) { + matches.push({ left: pool[index], right: pool[index + 1] || null, winner: pool[index + 1] ? null : pool[index] }); + } + return matches; +} + function create(candidates) { - return { - round: 1, - pool: candidates.slice(), - pairIndex: 0, - winners: [], - eliminated: [], - decisions: [] - }; + var pool = candidates.slice(); + return { round: 1, pool: pool, matches: buildMatches(pool), eliminated: [], decisions: [] }; } function current(state) { - if (state.pool.length === 1) return null; - return [state.pool[state.pairIndex], state.pool[state.pairIndex + 1] || null]; + var match = state.matches.filter(function (item) { return item.right && !item.winner; })[0]; + return match ? [match.left, match.right] : null; +} + +function advance(state) { + if (state.matches.some(function (item) { return !item.winner; })) return false; + state.pool = state.matches.map(function (item) { return item.winner; }); + if (state.pool.length === 1) return true; + state.round += 1; + state.matches = buildMatches(state.pool); + return false; +} + +function chooseAt(state, matchIndex, winner) { + var match = state.matches[matchIndex]; + if (!match || !match.right || match.winner) throw new Error('invalid bracket match'); + if (winner.id !== match.left.id && winner.id !== match.right.id) throw new Error('winner is not in match'); + var loser = match.left.id === winner.id ? match.right : match.left; + match.winner = winner; + state.eliminated.unshift(loser); + state.decisions.push({ round: state.round, left_id: match.left.id, right_id: match.right.id, winner_id: winner.id }); + return advance(state); } function choose(state, winner) { - var pair = current(state); - if (!pair || !pair[1]) throw new Error('invalid bracket pair'); - var loser = pair[0].id === winner.id ? pair[1] : pair[0]; - state.winners.push(winner); - state.eliminated.unshift(loser); - state.decisions.push({ round: state.round, left_id: pair[0].id, right_id: pair[1].id, winner_id: winner.id }); - state.pairIndex += 2; - if (state.pairIndex < state.pool.length) { - if (!state.pool[state.pairIndex + 1]) { - state.winners.push(state.pool[state.pairIndex]); - state.pairIndex += 1; - } - if (state.pairIndex < state.pool.length) return false; - } - state.pool = state.winners; - state.winners = []; - state.pairIndex = 0; - state.round += 1; - return state.pool.length === 1; + var index = state.matches.findIndex(function (item) { return item.right && !item.winner; }); + return chooseAt(state, index, winner); } function ranking(state) { return state.pool.concat(state.eliminated).map(function (item) { return item.id; }); } -module.exports = { create: create, current: current, choose: choose, ranking: ranking }; - +module.exports = { create: create, current: current, choose: choose, chooseAt: chooseAt, ranking: ranking }; diff --git a/vendor/poster-editor.cjs b/vendor/poster-editor.cjs new file mode 100644 index 0000000..9d41566 --- /dev/null +++ b/vendor/poster-editor.cjs @@ -0,0 +1,5 @@ +'use strict'; +function hash(value){var h=2166136261,s=String(value);for(var i=0;i>>0;} +function glyphAngle(seed,layer,index,max){return((hash(seed+':'+layer+':'+index)/4294967295)*2-1)*Math.abs(max||0);} +function createDocument(options){options=options||{};return{schema:'entroia.poster-document',version:1,id:options.id||('doc-'+Date.now()),seed:options.seed||('seed-'+Date.now()),canvas:{width:options.width||1080,height:options.height||1440,background:options.background||'#fff'},layers:options.layers||[],bindings:options.bindings||{},meta:options.meta||{}};} +module.exports={stableHash:hash,deterministicGlyphAngle:glyphAngle,createDocument:createDocument}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..356ec43 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: 'web', + base: './', + build: { outDir: '../dist', emptyOutDir: true } +}); diff --git a/web/app.js b/web/app.js index b79d71b..ba159dd 100644 --- a/web/app.js +++ b/web/app.js @@ -1,8 +1,10 @@ +import { animate, stagger } from 'motion'; + (function () { 'use strict'; var root = document.getElementById('app'); var apiBase = (window.PK_CONFIG.apiBase || '').replace(/\/$/, ''); - var state = { tests: [], test: null, bracket: null, result: null, template: 'celebration', sticker: true }; + var state = { tests: [], test: null, bracket: null, result: null, template: 'celebration', sticker: true, mode: 'single', bracketLayout: 'symmetric', bracketView: 'overview' }; function toast(message) { var el = document.getElementById('toast'); el.textContent = message; el.classList.add('show'); setTimeout(function () { el.classList.remove('show'); }, 2200); } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, function (char) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[char]; }); } @@ -23,24 +25,31 @@ request('/api/v1/tests').then(function (tests) { state.tests = tests; renderHome(); }).catch(renderError); } function renderHome() { - root.innerHTML = '
TASTE LAB · PUBLIC PREVIEW

今天,认真选一个。

没有“都可以”。每轮只选一个,让你的偏爱一路赢到底。

全部测试

' + state.tests.length + ' 个题库
' + state.tests.map(function (test, index) { - return ''; + root.innerHTML = '
PK
偏爱研究所
' + state.tests.map(function (test) { + return ''; }).join('') + '
'; root.querySelectorAll('.test-card').forEach(function (el) { el.addEventListener('click', function () { loadTest(el.dataset.slug); }); }); + document.getElementById('searchTests').addEventListener('input', function (event) { var value = event.target.value.toLowerCase(); root.querySelectorAll('.test-card').forEach(function (card) { card.hidden = card.dataset.search.toLowerCase().indexOf(value) < 0; }); }); + animate('.test-card', { opacity: [0, 1], y: [20, 0] }, { delay: stagger(.045), duration: .35 }); } - function loadTest(slug) { root.innerHTML = '
正在抽签…
'; request('/api/v1/tests/' + slug).then(function (test) { state.test = test; state.bracket = {round:1,pool:test.candidates.slice(),index:0,winners:[],eliminated:[],decisions:[]}; renderPair(); }).catch(renderError); } - function pair() { return [state.bracket.pool[state.bracket.index], state.bracket.pool[state.bracket.index + 1] || null]; } + function buildMatches(pool) { var matches=[];for(var i=0;i' + done + ' / ' + total + '

此刻只能留一个,
你会选谁?

VS
'; - root.querySelectorAll('.choice').forEach(function (el) { el.addEventListener('click', function () { choose(items[Number(el.dataset.side)]); }); }); + var tabs='
'; + var content; + if(state.mode==='multi'){var block=state.bracket.matches.slice(state.bracket.offset,state.bracket.offset+4);content='
'+block.map(function(m,i){var mi=state.bracket.offset+i;return m.winner?'
晋级
'+escapeHtml(m.winner.name)+'
':'
VS
';}).join('')+'
';}else{content='
VS
';} + root.innerHTML = '
第 ' + state.bracket.round + ' 轮' + done + ' / ' + total + '
'+tabs+content+'
'; + root.querySelectorAll('[data-mode]').forEach(function(el){el.classList.toggle('active',el.dataset.mode===state.mode);el.onclick=function(){state.mode=el.dataset.mode;renderPair();};}); + root.querySelectorAll('.choice').forEach(function (el) { el.addEventListener('click', function () { chooseAt(state.bracket.matches.findIndex(function(m){return m.right&&!m.winner;}),Number(el.dataset.side)); }); }); + root.querySelectorAll('[data-match]').forEach(function(el){el.onclick=function(){chooseAt(Number(el.dataset.match),Number(el.dataset.side));};}); + var swipe=root.querySelector('.swipeable'),startX;if(swipe){swipe.onpointerdown=function(e){startX=e.clientX;};swipe.onpointerup=function(e){var d=e.clientX-startX;if(state.mode==='swipe'&&Math.abs(d)>60)chooseAt(state.bracket.matches.findIndex(function(m){return m.right&&!m.winner;}),d<0?0:1);};} } - function choose(winner) { - var b = state.bracket, items = pair(), loser = items[0].id === winner.id ? items[1] : items[0]; - b.winners.push(winner); b.eliminated.unshift(loser); b.decisions.push({round:b.round,left_id:items[0].id,right_id:items[1].id,winner_id:winner.id}); b.index += 2; - if (b.index < b.pool.length && !b.pool[b.index + 1]) { b.winners.push(b.pool[b.index]); b.index += 1; } - if (b.index >= b.pool.length) { b.pool = b.winners; b.winners = []; b.index = 0; b.round += 1; } - if (b.pool.length === 1) complete(); else renderPair(); + function chooseAt(index,side) { + var b=state.bracket,m=b.matches[index];if(!m||m.winner)return;var winner=side?m.right:m.left,loser=side?m.left:m.right;m.winner=winner;b.eliminated.unshift(loser);b.decisions.push({round:b.round,left_id:m.left.id,right_id:m.right.id,winner_id:winner.id}); + if(b.matches.every(function(item){return item.winner;})){b.pool=b.matches.map(function(item){return item.winner;});if(b.pool.length===1){complete();return;}b.round++;b.matches=buildMatches(b.pool);b.offset=0;}else if(state.mode==='multi'){var block=b.matches.slice(b.offset,b.offset+4);if(block.length&&block.every(function(item){return item.winner;}))b.offset+=4;} + renderPair(); } function complete() { var b = state.bracket, champion = b.pool[0], final = b.decisions[b.decisions.length - 1], runner = final.left_id === champion.id ? final.right_id : final.left_id; @@ -49,19 +58,22 @@ } function renderResult() { var r = state.result; - root.innerHTML = '
FINAL ANSWER · ' + escapeHtml(r.test_title) + '

' + escapeHtml(r.champion_name) + '

' + (r.runner_up_name ? '决赛击败 ' + escapeHtml(r.runner_up_name) : '一路赢到最后') + '

'; + root.innerHTML = '
分享图编辑器

'+escapeHtml(r.test_title)+' · '+escapeHtml(r.champion_name)+'

'; drawPoster(); root.querySelectorAll('[data-theme]').forEach(function (el) { el.addEventListener('click', function () { state.template = el.dataset.theme; root.querySelectorAll('[data-theme]').forEach(function(x){x.classList.remove('active');});el.classList.add('active');drawPoster(); }); }); + root.querySelectorAll('[data-layout]').forEach(function(el){el.onclick=function(){state.bracketLayout=el.dataset.layout;drawPoster();};}); + root.querySelectorAll('[data-view]').forEach(function(el){el.onclick=function(){state.bracketView=el.dataset.view;drawPoster();};}); document.getElementById('stickerToggle').addEventListener('click',function(){state.sticker=!state.sticker;this.textContent='手绘贴纸:'+(state.sticker?'开启':'关闭');drawPoster();}); document.getElementById('savePoster').addEventListener('click',function(){var a=document.createElement('a');a.download='我的PK冠军-'+r.champion_name+'.png';a.href=document.getElementById('poster').toDataURL('image/png');a.click();toast('分享图已保存');}); document.getElementById('replay').addEventListener('click',function(){loadTest(r.test_slug);}); } function drawPoster() { var canvas=document.getElementById('poster'),ctx=canvas.getContext('2d'),r=state.result,themes={celebration:['#ff6b35','#fffaf4','#25211f','#ffe568'],editorial:['#f3eadf','#201d1b','#fff','#285ee8'],sticker:['#c9f06b','#1e2810','#ff72a7','#fff']},t=themes[state.template]; - ctx.fillStyle=t[0];ctx.fillRect(0,0,750,1000);ctx.fillStyle=t[1];ctx.font='700 24px system-ui';ctx.fillText('PK 研究所 / MY PICK',54,72);ctx.font='900 58px system-ui';ctx.fillText('我的冠军是',54,156);ctx.fillStyle=t[2];ctx.fillRect(46,205,658,440);ctx.fillStyle=state.template==='celebration'?'#fffaf4':t[1];ctx.font='600 26px system-ui';ctx.fillText(r.test_title,84,270);ctx.font='900 '+(r.champion_name.length>7?62:82)+'px system-ui';ctx.fillText(r.champion_name,84,405);ctx.fillStyle=t[3];ctx.fillRect(84,440,Math.min(500,r.champion_name.length*80),18);ctx.fillStyle=state.template==='celebration'?'#fffaf4':t[1];ctx.font='24px system-ui';ctx.fillText('一路二选一,它赢到了最后。',84,525);ctx.fillStyle=t[1];ctx.font='700 25px system-ui';ctx.fillText('我的 TOP 4',54,720);(r.ranking_names||[]).slice(0,4).forEach(function(name,i){ctx.font='26px system-ui';ctx.fillText((i+1)+'. '+name,64,774+i*48);});if(state.sticker){ctx.fillStyle=t[3];ctx.beginPath();ctx.arc(645,125,58,0,Math.PI*2);ctx.fill();ctx.fillStyle=t[2];ctx.font='900 28px system-ui';ctx.fillText('WIN!',607,134);ctx.font='52px system-ui';ctx.fillText('✦',640,700);ctx.fillText('♡',550,900);}ctx.fillStyle=t[1];ctx.font='20px system-ui';ctx.fillText('保存这张图,把答案分享给同频的人',54,956); + ctx.fillStyle=t[0];ctx.fillRect(0,0,750,1000);ctx.fillStyle=t[1];ctx.font='700 24px system-ui';ctx.fillText('PK 研究所 / BRACKET',42,62);ctx.font='900 48px system-ui';ctx.fillText(r.test_title,42,125);ctx.fillStyle=t[2];ctx.fillRect(28,165,694,690);drawBracketWeb(ctx,r,t);if(state.sticker){ctx.fillStyle=t[3];ctx.beginPath();ctx.arc(660,100,48,0,Math.PI*2);ctx.fill();ctx.fillStyle=t[2];ctx.font='900 22px system-ui';ctx.fillText('WIN',635,108);}ctx.fillStyle=t[1];ctx.font='700 25px system-ui';ctx.fillText('冠军 '+r.champion_name,42,915);ctx.font='18px system-ui';ctx.fillText('保存分享图,看看朋友的晋级路线',42,962); } + function drawBracketWeb(ctx,r,t){var ds=r.decisions||[],names=r.candidate_names||{};if(!ds.length){ctx.fillStyle=t[1];ctx.font='900 72px system-ui';ctx.fillText(r.champion_name,70,500);return;}var max=ds.reduce(function(m,d){return Math.max(m,d.round);},1),min=state.bracketView==='final'?max:state.bracketView==='top8'?Math.max(1,max-2):state.bracketView==='top16'?Math.max(1,max-3):1,visible=ds.filter(function(d,i){if(d.roundp.d.round&&(d.left_id===p.d.winner_id||d.right_id===p.d.winner_id))parent=pos[d.winner_id+':'+d.round];});if(parent){ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo((p.x+parent.x)/2,p.y);ctx.lineTo((p.x+parent.x)/2,parent.y);ctx.lineTo(parent.x,parent.y);ctx.stroke();}});Object.keys(pos).forEach(function(k){var p=pos[k],label=names[p.d.winner_id]||r.champion_name;ctx.fillStyle=p.d.round===max?t[3]:t[0];ctx.fillRect(p.x-56,p.y-20,112,40);ctx.fillStyle=t[1];ctx.font=(label.length>6?'13':'16')+'px system-ui';ctx.fillText(label.slice(0,8),p.x-47,p.y+6);});} function loadHistory() { root.innerHTML='
正在翻档案…
';ensureLogin().then(function(){return request('/api/v1/me/results');}).then(renderHistory).catch(renderError); } + function loadDiscover() { root.innerHTML='
正在收集答案…
';request('/api/v1/results/discover').then(function(results){root.innerHTML='
PK
大家最近选了什么
'+results.map(function(r){return '

'+escapeHtml(r.test_title)+'

'+escapeHtml(r.champion_name)+'

';}).join('')+'
';animate('.test-card',{opacity:[0,1],scale:[.96,1]},{delay:stagger(.04)});}).catch(renderError); } function renderHistory(results) { root.innerHTML='
MY TASTE ARCHIVE

我的偏爱档案

每一次纠结,都算数。

做过的测试

'+results.length+' 条结果
'+(results.length?results.map(function(r,i){return '';}).join(''):'
还没有结果,去选第一个冠军吧。
')+'
';document.getElementById('linkCode').addEventListener('click',function(){request('/api/v1/me/link-code',{method:'POST'}).then(function(d){document.getElementById('code').textContent=d.code;}).catch(renderError);});root.querySelectorAll('.result-row').forEach(function(el){el.addEventListener('click',function(){state.result=results[Number(el.dataset.i)];renderResult();});}); } function renderError(error) { root.innerHTML='

暂时没接上服务

'+escapeHtml(error.message||'未知错误')+'

';document.getElementById('retry').addEventListener('click',loadHome); } - document.getElementById('homeButton').addEventListener('click',loadHome); document.getElementById('meButton').addEventListener('click',loadHistory); loadHome(); + document.getElementById('homeButton').addEventListener('click',loadHome); document.getElementById('discoverButton').addEventListener('click',loadDiscover); document.getElementById('meButton').addEventListener('click',loadHistory); loadHome(); }()); - diff --git a/web/arena.css b/web/arena.css new file mode 100644 index 0000000..79baf89 --- /dev/null +++ b/web/arena.css @@ -0,0 +1 @@ +.mode-tabs{display:flex;width:320px;margin:18px auto;padding:5px;border-radius:8px;background:#eee}.mode-tabs button{flex:1;padding:10px;border-radius:6px;background:transparent;color:#777}.mode-tabs button.active{background:#111;color:#fff}.arena{height:calc(100vh - 110px);overflow:hidden;margin-top:24px}.arena .pair{height:calc(100% - 120px)}.arena .choice{border-radius:8px}.multi-pairs{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:10px;height:calc(100% - 110px)}.mini-pair{display:grid;grid-template-columns:1fr 40px 1fr;min-height:0;border:2px solid #111;background:#fff}.mini-pair button{padding:14px;background:#fff;font-weight:800}.mini-pair span{display:grid;place-items:center;background:#111;color:#fff;font-size:12px}.mini-pair.winner{display:grid;place-items:center;text-align:center;background:#111;color:#fff;animation:winner-in .32s cubic-bezier(.2,1.4,.4,1)}@keyframes winner-in{from{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}@media(max-width:700px){.arena{height:calc(100vh - 95px);width:calc(100% - 20px)}.arena .pair{grid-template-columns:1fr 1fr}.arena .choice{min-height:0}.multi-pairs{gap:6px}.mini-pair{grid-template-columns:1fr 28px 1fr}.mini-pair button{padding:8px;font-size:13px}} diff --git a/web/editor.css b/web/editor.css new file mode 100644 index 0000000..0876852 --- /dev/null +++ b/web/editor.css @@ -0,0 +1 @@ +.editor-shell{padding-bottom:100px}.editor-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.editor-title h2{margin:5px 0 0}.editor-title small{color:#777}.control-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.control-grid .theme{margin:0}.editor-tools{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:18px}.editor-tools button{padding:12px 4px;border:1px solid #ddd;border-radius:8px;background:#fff;font-size:12px}.editor-shell .poster{border-radius:8px}.editor-shell .control{border-radius:8px}@media(max-width:800px){.editor-title{align-items:flex-end}.editor-title h2{font-size:17px}.editor-shell .result-layout{grid-template-columns:minmax(0,1fr)}.editor-shell .control{padding:18px}} diff --git a/web/home.css b/web/home.css new file mode 100644 index 0000000..1d64b9b --- /dev/null +++ b/web/home.css @@ -0,0 +1 @@ +.home-shell{max-width:920px;margin-top:24px;padding-bottom:100px}.mast{display:flex;align-items:center;gap:14px;margin:0 0 20px;font-size:24px}.logo-mark{display:grid;width:54px;height:42px;place-items:center;background:#111;color:#fff;font-weight:950;letter-spacing:-2px}.search{display:flex;align-items:center;gap:10px;margin-bottom:18px;padding:13px 16px;background:#f2f2f2;border-radius:5px}.search input{width:100%;border:0;outline:0;background:transparent;font:inherit}.home-shell .grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:start}.home-shell .test-card{border-radius:0;box-shadow:none}.home-shell .test-card:nth-child(3n+2) .art{height:390px}.home-shell .art{display:block;width:100%;height:330px;object-fit:cover}.home-shell .card-body{padding:14px 4px 22px}.bottom-nav{position:fixed;z-index:40;left:50%;bottom:22px;display:flex;width:min(360px,calc(100% - 36px));height:72px;padding:0 12px;transform:translateX(-50%);border-radius:16px;background:#080808;box-shadow:0 16px 44px rgba(0,0,0,.28)}.nav-tab{position:relative;flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;background:none;color:#888;font-size:12px}.nav-tab i{width:18px;height:18px;margin-bottom:5px;border:3px solid currentColor;border-radius:50%}.nav-tab:first-child{color:#fff}.nav-tab:first-child:before{content:"";position:absolute;top:-17px;width:62px;height:62px;border:9px solid #fff;border-radius:50%;background:#080808}.nav-tab i,.nav-tab span{z-index:1}@media(max-width:520px){.home-shell{width:calc(100% - 16px)}.home-shell .grid{gap:8px}.home-shell .art{height:250px}.home-shell .test-card:nth-child(3n+2) .art{height:300px}.home-shell .card-body{padding:12px 3px 18px}.choice-emoji{display:none}} diff --git a/web/index.html b/web/index.html index ec3539e..1e130cb 100644 --- a/web/index.html +++ b/web/index.html @@ -6,13 +6,15 @@ PK 研究所 · 公网预览 + + + -
+
- - + + -