add tinder view
This commit is contained in:
114
package-lock.json
generated
114
package-lock.json
generated
@@ -10,11 +10,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ionic/react": "^8.0.0",
|
"@ionic/react": "^8.0.0",
|
||||||
"@ionic/react-router": "^8.0.0",
|
"@ionic/react-router": "^8.0.0",
|
||||||
|
"@react-spring/web": "^9.7.5",
|
||||||
"ionicons": "^7.2.0",
|
"ionicons": "^7.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4"
|
"react-router-dom": "^5.3.4",
|
||||||
|
"react-tinder-card": "^1.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
@@ -58,7 +60,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1539,6 +1540,78 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-spring/animated": {
|
||||||
|
"version": "9.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
|
||||||
|
"integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/shared": "~9.7.5",
|
||||||
|
"@react-spring/types": "~9.7.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/core": {
|
||||||
|
"version": "9.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
|
||||||
|
"integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/animated": "~9.7.5",
|
||||||
|
"@react-spring/shared": "~9.7.5",
|
||||||
|
"@react-spring/types": "~9.7.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-spring/donate"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/rafz": {
|
||||||
|
"version": "9.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
|
||||||
|
"integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/shared": {
|
||||||
|
"version": "9.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
|
||||||
|
"integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/rafz": "~9.7.5",
|
||||||
|
"@react-spring/types": "~9.7.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/types": {
|
||||||
|
"version": "9.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
|
||||||
|
"integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/web": {
|
||||||
|
"version": "9.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz",
|
||||||
|
"integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/animated": "~9.7.5",
|
||||||
|
"@react-spring/core": "~9.7.5",
|
||||||
|
"@react-spring/shared": "~9.7.5",
|
||||||
|
"@react-spring/types": "~9.7.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@@ -2003,7 +2076,6 @@
|
|||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -2100,7 +2172,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2448,6 +2519,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-sleep": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-sleep/-/p-sleep-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-bwP3GKZirBUYMtiUuBrheLUQdRXVeE/pmHOaLpNJzNfAD4b5AjDn6l823brXcQFade4G/g7GMNQ3KV86E8EaEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||||
@@ -2516,7 +2593,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -2529,7 +2605,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -2559,7 +2634,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
||||||
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@@ -2580,7 +2654,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
||||||
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@@ -2594,6 +2667,28 @@
|
|||||||
"react": ">=15"
|
"react": ">=15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-tinder-card": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-tinder-card/-/react-tinder-card-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-IC6YXoBZ+51jm7XsT8i+8G/ov8rvAob+kBRdp9unQyjsLc7jmuYb1cNfu95Q3mdFDgwE0AzTIyl1o2Klm61+aQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-sleep": "^1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-spring/native": "^9.5.5",
|
||||||
|
"@react-spring/web": "^9.5.5",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@react-spring/native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@react-spring/web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-pathname": {
|
"node_modules/resolve-pathname": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||||
@@ -2905,7 +3000,6 @@
|
|||||||
"integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
|
"integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
@@ -2953,7 +3047,6 @@
|
|||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -3015,7 +3108,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"workerd": "bin/workerd"
|
"workerd": "bin/workerd"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,11 +20,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ionic/react": "^8.0.0",
|
"@ionic/react": "^8.0.0",
|
||||||
"@ionic/react-router": "^8.0.0",
|
"@ionic/react-router": "^8.0.0",
|
||||||
|
"@react-spring/web": "^9.7.5",
|
||||||
"ionicons": "^7.2.0",
|
"ionicons": "^7.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4"
|
"react-router-dom": "^5.3.4",
|
||||||
|
"react-tinder-card": "^1.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
|
|||||||
@@ -4,20 +4,215 @@
|
|||||||
--padding-end: 16px;
|
--padding-end: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discover-placeholder {
|
/* States: loading, empty */
|
||||||
|
.discover-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-state p {
|
||||||
|
margin: 0;
|
||||||
|
color: #8e8e93;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-state-hint {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Done state */
|
||||||
|
.discover-done {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discover-placeholder h2 {
|
.discover-done h2 {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 1.5rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discover-placeholder p {
|
.discover-done-stats {
|
||||||
margin: 0;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-done-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-done-stat strong {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-done-stat span {
|
||||||
|
font-size: 0.85rem;
|
||||||
color: #8e8e93;
|
color: #8e8e93;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
.discover-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-progress span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8e8e93;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: #e5e5ea;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--ion-color-primary, #0a84ff);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card stack */
|
||||||
|
.discover-stack {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
max-height: 55vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-stack > div {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game card */
|
||||||
|
.discover-card {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 1.75rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
cursor: grab;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card-behind {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card content */
|
||||||
|
.discover-card-source {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card-detail-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #8e8e93;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-card-detail-value {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action buttons */
|
||||||
|
.discover-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-action-btn {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-action-btn:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-action-skip {
|
||||||
|
border-color: #ff3b30;
|
||||||
|
color: #ff3b30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-action-skip:hover {
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-action-like {
|
||||||
|
border-color: #34c759;
|
||||||
|
color: #34c759;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-action-like:hover {
|
||||||
|
background: #f0faf3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,152 @@
|
|||||||
import {
|
import {
|
||||||
|
IonAlert,
|
||||||
|
IonBadge,
|
||||||
|
IonButton,
|
||||||
IonContent,
|
IonContent,
|
||||||
IonHeader,
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
IonPage,
|
IonPage,
|
||||||
|
IonSpinner,
|
||||||
IonTitle,
|
IonTitle,
|
||||||
IonToolbar,
|
IonToolbar,
|
||||||
} from "@ionic/react";
|
} from "@ionic/react";
|
||||||
|
import {
|
||||||
|
closeOutline,
|
||||||
|
checkmarkOutline,
|
||||||
|
refreshOutline,
|
||||||
|
} from "ionicons/icons";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import TinderCard from "react-tinder-card";
|
||||||
|
import { db, type Game } from "../../services/Database";
|
||||||
|
|
||||||
import "./DiscoverPage.css";
|
import "./DiscoverPage.css";
|
||||||
|
|
||||||
|
type SwipeResults = Record<string, "skip" | "interested">;
|
||||||
|
|
||||||
|
const formatDate = (value?: string | null) => {
|
||||||
|
if (!value) return "-";
|
||||||
|
return new Date(value).toLocaleDateString("de");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPlaytime = (hours?: number) => {
|
||||||
|
if (!hours) return "0 h";
|
||||||
|
if (hours < 1) return `${Math.round(hours * 60)} min`;
|
||||||
|
return `${hours.toFixed(1)} h`;
|
||||||
|
};
|
||||||
|
|
||||||
export default function DiscoverPage() {
|
export default function DiscoverPage() {
|
||||||
|
const [games, setGames] = useState<Game[]>([]);
|
||||||
|
const [swipeResults, setSwipeResults] = useState<SwipeResults>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showResetAlert, setShowResetAlert] = useState(false);
|
||||||
|
|
||||||
|
const cardRefs = useRef<Map<number, any>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [dbGames, savedResults] = await Promise.all([
|
||||||
|
db.getGames(),
|
||||||
|
db.getSetting("swipe_results"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
setGames(dbGames);
|
||||||
|
setSwipeResults(savedResults || {});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (active) setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unseenGames = useMemo(
|
||||||
|
() => games.filter((g) => !(g.id in swipeResults)),
|
||||||
|
[games, swipeResults],
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveSwipe = useCallback(
|
||||||
|
async (gameId: string, decision: "skip" | "interested") => {
|
||||||
|
const updated = { ...swipeResults, [gameId]: decision };
|
||||||
|
setSwipeResults(updated);
|
||||||
|
await db.setSetting("swipe_results", updated);
|
||||||
|
},
|
||||||
|
[swipeResults],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSwipe = useCallback(
|
||||||
|
(direction: string, gameId: string) => {
|
||||||
|
const decision = direction === "right" ? "interested" : "skip";
|
||||||
|
saveSwipe(gameId, decision);
|
||||||
|
},
|
||||||
|
[saveSwipe],
|
||||||
|
);
|
||||||
|
|
||||||
|
const swipeButton = useCallback(
|
||||||
|
(direction: "left" | "right") => {
|
||||||
|
if (unseenGames.length === 0) return;
|
||||||
|
const topGame = unseenGames[unseenGames.length - 1];
|
||||||
|
const topIndex = games.indexOf(topGame);
|
||||||
|
const ref = cardRefs.current.get(topIndex);
|
||||||
|
if (ref) {
|
||||||
|
ref.swipe(direction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[unseenGames, games],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleReset = useCallback(async () => {
|
||||||
|
setSwipeResults({});
|
||||||
|
await db.setSetting("swipe_results", {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const totalSwiped = Object.keys(swipeResults).length;
|
||||||
|
const interestedCount = Object.values(swipeResults).filter(
|
||||||
|
(v) => v === "interested",
|
||||||
|
).length;
|
||||||
|
const skippedCount = totalSwiped - interestedCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IonPage>
|
<IonPage>
|
||||||
<IonHeader translucent>
|
<IonHeader translucent>
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle>Entdecken</IonTitle>
|
<IonTitle>Entdecken</IonTitle>
|
||||||
|
{totalSwiped > 0 && (
|
||||||
|
<IonButton
|
||||||
|
slot="end"
|
||||||
|
fill="clear"
|
||||||
|
onClick={() => setShowResetAlert(true)}
|
||||||
|
color="medium"
|
||||||
|
>
|
||||||
|
<IonIcon slot="icon-only" icon={refreshOutline} />
|
||||||
|
</IonButton>
|
||||||
|
)}
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonAlert
|
||||||
|
isOpen={showResetAlert}
|
||||||
|
onDidDismiss={() => setShowResetAlert(false)}
|
||||||
|
header="Zurucksetzen?"
|
||||||
|
message={`Alle ${totalSwiped} Swipe-Entscheidungen werden geloscht.`}
|
||||||
|
buttons={[
|
||||||
|
{ text: "Abbrechen", role: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Zurucksetzen",
|
||||||
|
role: "destructive",
|
||||||
|
handler: handleReset,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<IonContent fullscreen className="discover-content">
|
<IonContent fullscreen className="discover-content">
|
||||||
<IonHeader collapse="condense">
|
<IonHeader collapse="condense">
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
@@ -23,13 +154,136 @@ export default function DiscoverPage() {
|
|||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
<div className="discover-placeholder">
|
{loading ? (
|
||||||
<h2>Swipe & Entdecke</h2>
|
<div className="discover-state">
|
||||||
<p>
|
<IonSpinner name="crescent" />
|
||||||
Tinder-Style: Screenshots ansehen, bewerten und deinen perfekten
|
<p>Lade Spiele ...</p>
|
||||||
Gaming-Stack aufbauen.
|
</div>
|
||||||
</p>
|
) : games.length === 0 ? (
|
||||||
</div>
|
<div className="discover-state">
|
||||||
|
<p>Keine Spiele vorhanden.</p>
|
||||||
|
<p className="discover-state-hint">
|
||||||
|
Importiere zuerst Spiele in den Einstellungen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : unseenGames.length === 0 ? (
|
||||||
|
<div className="discover-done">
|
||||||
|
<h2>Alle Spiele gesehen!</h2>
|
||||||
|
<div className="discover-done-stats">
|
||||||
|
<div className="discover-done-stat">
|
||||||
|
<strong>{interestedCount}</strong>
|
||||||
|
<span>Interessiert</span>
|
||||||
|
</div>
|
||||||
|
<div className="discover-done-stat">
|
||||||
|
<strong>{skippedCount}</strong>
|
||||||
|
<span>Ubersprungen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IonButton
|
||||||
|
expand="block"
|
||||||
|
fill="outline"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
<IonIcon slot="start" icon={refreshOutline} />
|
||||||
|
Nochmal starten
|
||||||
|
</IonButton>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="discover-progress">
|
||||||
|
<span>
|
||||||
|
{totalSwiped} / {games.length} Spiele
|
||||||
|
</span>
|
||||||
|
<div className="discover-progress-bar">
|
||||||
|
<div
|
||||||
|
className="discover-progress-fill"
|
||||||
|
style={{
|
||||||
|
width: `${(totalSwiped / games.length) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discover-stack">
|
||||||
|
{unseenGames
|
||||||
|
.slice(-3)
|
||||||
|
.map((game, i, arr) => {
|
||||||
|
const globalIndex = games.indexOf(game);
|
||||||
|
const stackPosition = arr.length - 1 - i;
|
||||||
|
return (
|
||||||
|
<TinderCard
|
||||||
|
ref={(ref: any) => {
|
||||||
|
if (ref) {
|
||||||
|
cardRefs.current.set(globalIndex, ref);
|
||||||
|
} else {
|
||||||
|
cardRefs.current.delete(globalIndex);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={game.id}
|
||||||
|
onSwipe={(dir: string) =>
|
||||||
|
handleSwipe(dir, game.id)
|
||||||
|
}
|
||||||
|
preventSwipe={["up", "down"]}
|
||||||
|
swipeRequirementType="position"
|
||||||
|
swipeThreshold={80}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`discover-card ${stackPosition > 0 ? "discover-card-behind" : ""}`}
|
||||||
|
style={{
|
||||||
|
zIndex: arr.length - stackPosition,
|
||||||
|
transform: `scale(${1 - stackPosition * 0.04}) translateY(${stackPosition * 12}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="discover-card-source">
|
||||||
|
<IonBadge color="medium">
|
||||||
|
{game.source ?? "Unbekannt"}
|
||||||
|
</IonBadge>
|
||||||
|
</div>
|
||||||
|
<h2 className="discover-card-title">
|
||||||
|
{game.title}
|
||||||
|
</h2>
|
||||||
|
<div className="discover-card-details">
|
||||||
|
<div className="discover-card-detail">
|
||||||
|
<span className="discover-card-detail-label">
|
||||||
|
Spielzeit
|
||||||
|
</span>
|
||||||
|
<span className="discover-card-detail-value">
|
||||||
|
{formatPlaytime(
|
||||||
|
game.playtimeHours,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="discover-card-detail">
|
||||||
|
<span className="discover-card-detail-label">
|
||||||
|
Zuletzt gespielt
|
||||||
|
</span>
|
||||||
|
<span className="discover-card-detail-value">
|
||||||
|
{formatDate(game.lastPlayed)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TinderCard>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discover-actions">
|
||||||
|
<button
|
||||||
|
className="discover-action-btn discover-action-skip"
|
||||||
|
onClick={() => swipeButton("left")}
|
||||||
|
>
|
||||||
|
<IonIcon icon={closeOutline} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="discover-action-btn discover-action-like"
|
||||||
|
onClick={() => swipeButton("right")}
|
||||||
|
>
|
||||||
|
<IonIcon icon={checkmarkOutline} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user