6 Blog post
|
|
@ -0,0 +1,41 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "curated-finds-working",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 641 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 597 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 540 KiB |
|
After Width: | Height: | Size: 644 KiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 855 KiB |
|
After Width: | Height: | Size: 746 KiB |
|
After Width: | Height: | Size: 805 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,327 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Abril+Fatface&family=Spectral:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Staatliches&family=Pacifico&display=swap');
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F7F1E1;
|
||||
color: #1E1A17;
|
||||
font-family: 'Spectral', serif;
|
||||
background-image: radial-gradient(circle at 1px 1px, rgba(30, 26, 23, 0.03) 1px, transparent 0);
|
||||
background-size: 20px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.02'/%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.vintage-header {
|
||||
position: relative;
|
||||
background-color: #F7F1E1;
|
||||
border-bottom: 2px solid #8B7D6B;
|
||||
box-shadow: 0 2px 4px rgba(30, 26, 23, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
border-bottom: 1px solid #8B7D6B;
|
||||
}
|
||||
|
||||
.masthead h1 {
|
||||
font-family: 'Abril Fatface', serif;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
color: #1E1A17;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.masthead .subtitle {
|
||||
font-family: 'Abril Fatface', serif;
|
||||
font-size: 28px;
|
||||
color: #C89C2B;
|
||||
margin-top: -8px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.newspaper-rule {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #8B7D6B, transparent);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
color: #8B7D6B;
|
||||
}
|
||||
|
||||
.nav-ribbon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-family: 'Staatliches', sans-serif;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 8px 12px;
|
||||
color: #1E1A17;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: #C89C2B;
|
||||
}
|
||||
|
||||
.nav-links a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #C89C2B;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-links a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-separator {
|
||||
color: #8B7D6B;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.ticket-button {
|
||||
background: #C89C2B;
|
||||
color: #F7F1E1;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: bold;
|
||||
border-radius: 0 12px 12px 0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ticket-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 3px,
|
||||
#F7F1E1 3px,
|
||||
#F7F1E1 6px
|
||||
);
|
||||
}
|
||||
|
||||
.ticket-button::after {
|
||||
content: 'ADMIT 001';
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 4px;
|
||||
font-size: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.stamp-button {
|
||||
background: transparent;
|
||||
color: #C89C2B;
|
||||
border: 2px dashed #C89C2B;
|
||||
padding: 12px 24px;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.stamp-button:hover {
|
||||
background: #C89C2B;
|
||||
color: #F7F1E1;
|
||||
box-shadow: inset 0 0 20px rgba(200, 156, 43, 0.3);
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero-section {
|
||||
padding: 64px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-seal {
|
||||
position: absolute;
|
||||
top: -32px;
|
||||
right: -16px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: #7B2E2E;
|
||||
color: #F7F1E1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Pacifico', cursive;
|
||||
font-size: 12px;
|
||||
transform: rotate(-12deg);
|
||||
box-shadow: 0 2px 8px rgba(123, 46, 46, 0.3);
|
||||
z-index: 10;
|
||||
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.editor-seal:hover {
|
||||
transform: rotate(-10deg) scale(1.05);
|
||||
}
|
||||
|
||||
.drop-cap-text {
|
||||
font-family: 'Spectral', serif;
|
||||
font-size: 24px;
|
||||
line-height: 1.6;
|
||||
color: #1E1A17;
|
||||
max-width: 800px;
|
||||
margin: 0 auto 32px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.drop-cap-text::first-letter {
|
||||
font-family: 'Abril Fatface', serif;
|
||||
font-size: 72px;
|
||||
float: left;
|
||||
line-height: 1;
|
||||
margin-right: 8px;
|
||||
margin-top: 4px;
|
||||
color: #C89C2B;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.hero-subheading {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
color: #8B7D6B;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.hero-ctas {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.trust-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
padding: 12px 24px;
|
||||
border: 1px solid #8B7D6B;
|
||||
}
|
||||
|
||||
.trust-badge-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #2D6A6A;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trust-badge-text {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #8B7D6B;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.masthead h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.masthead .subtitle {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.drop-cap-text::first-letter {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.hero-ctas {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import { AppProvider } from '@/contexts/AppContext'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'The Curated Finds Blog',
|
||||
description: 'Handpicked treasures from the eBay universe — updated weekly by experts who know quality when they see it',
|
||||
keywords: ['vintage', 'collectibles', 'eBay', 'curated', 'affordable', 'authentic'],
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AppProvider>
|
||||
{children}
|
||||
</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
.page {
|
||||
--gray-rgb: 0, 0, 0;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 20px 1fr 20px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
min-height: 100svh;
|
||||
padding: 80px;
|
||||
gap: 64px;
|
||||
font-family: var(--font-geist-sans);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page {
|
||||
--gray-rgb: 255, 255, 255;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
font-family: var(--font-geist-mono);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.main li:not(:last-of-type) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.main code {
|
||||
font-family: inherit;
|
||||
background: var(--gray-alpha-100);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
appearance: none;
|
||||
border-radius: 128px;
|
||||
height: 48px;
|
||||
padding: 0 20px;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--gray-alpha-200);
|
||||
min-width: 158px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-row-start: 3;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
padding: 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ScrollEffects } from '@/components/ScrollEffects'
|
||||
import { FloatingElements } from '@/components/FloatingElements'
|
||||
import { ClientOnly } from '@/components/ClientOnly'
|
||||
import { BlogPostCard, BlogPostModal } from '@/components/BlogPost'
|
||||
import { blogPosts, BlogPost } from '@/data/blogPosts'
|
||||
|
||||
export default function HomePage() {
|
||||
const [selectedBlogPost, setSelectedBlogPost] = useState<BlogPost | null>(null)
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', backgroundColor: '#F7F1E1' }}>
|
||||
{/* Scroll Effects */}
|
||||
<ClientOnly>
|
||||
<ScrollEffects />
|
||||
</ClientOnly>
|
||||
|
||||
{/* Floating Background Elements */}
|
||||
<ClientOnly>
|
||||
<FloatingElements />
|
||||
</ClientOnly>
|
||||
{/* Header */}
|
||||
<header className="vintage-header">
|
||||
<div className="container">
|
||||
{/* Masthead */}
|
||||
<div className="masthead">
|
||||
<h1>The Curated Finds</h1>
|
||||
<div className="subtitle">BLOG</div>
|
||||
<div className="newspaper-rule"></div>
|
||||
<p className="tagline">Handpicked Treasures from the eBay Universe • Est. 2024</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
{/* Hero Section with Search */}
|
||||
<section className="hero-section">
|
||||
<div className="hero-content">
|
||||
{/* Editor's Seal */}
|
||||
<div className="editor-seal">
|
||||
Editor's<br />Choice
|
||||
</div>
|
||||
|
||||
{/* Main Headline with Drop Cap */}
|
||||
<div>
|
||||
<p className="drop-cap-text">
|
||||
Handpicked treasures from the eBay universe — updated weekly by experts who know quality when they see it. Every piece tells a story, every find has character, and every discovery connects you to the rich history of human craftsmanship.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="newspaper-rule"></div>
|
||||
|
||||
|
||||
{/* Subheading */}
|
||||
<p className="hero-subheading">
|
||||
Curated Finds • Curated Collections • Expert Insights
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="hero-ctas">
|
||||
<a href="#blog-posts" className="ticket-button">
|
||||
Read Collector's Chronicles
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Trust Badge */}
|
||||
<div className="trust-badge">
|
||||
<div className="trust-badge-icon">✓</div>
|
||||
<p className="trust-badge-text">No spam, just curated finds • 100% authentic</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Blog Posts Section */}
|
||||
<section id="blog-posts" style={{
|
||||
padding: '64px 20px',
|
||||
backgroundColor: '#F7F1E1',
|
||||
borderTop: '2px solid #8B7D6B'
|
||||
}}>
|
||||
<div className="container">
|
||||
<div style={{ textAlign: 'center', marginBottom: '48px' }}>
|
||||
<h2 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '48px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
Collector's Chronicles
|
||||
</h2>
|
||||
<div className="newspaper-rule"></div>
|
||||
<p style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '14px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.2em',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
Deep dives into remarkable finds
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
maxWidth: '800px',
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
{blogPosts.map((post) => (
|
||||
<BlogPostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onReadMore={setSelectedBlogPost}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer style={{
|
||||
backgroundColor: '#F7F1E1',
|
||||
borderTop: '2px solid #8B7D6B',
|
||||
padding: '32px 20px'
|
||||
}}>
|
||||
<div className="container">
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h3 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '24px',
|
||||
margin: 0,
|
||||
color: '#1E1A17'
|
||||
}}>
|
||||
The Curated Finds
|
||||
</h3>
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px',
|
||||
color: '#4A4A4A',
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
Handpicked treasures. Honest descriptions. Careful packaging.
|
||||
</p>
|
||||
<div className="newspaper-rule" style={{ margin: '16px auto', width: '120px' }} />
|
||||
<p style={{ color: '#8B7D6B', fontFamily: 'Space Mono, monospace', fontSize: '12px' }}>
|
||||
© {new Date().getFullYear()} The Curated Finds — All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Blog Post Modal */}
|
||||
{selectedBlogPost && (
|
||||
<BlogPostModal
|
||||
post={selectedBlogPost}
|
||||
onClose={() => setSelectedBlogPost(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
'use client'
|
||||
|
||||
import { CuratedItem } from '@/types'
|
||||
|
||||
interface AuthenticationBadgeProps {
|
||||
item: CuratedItem
|
||||
detailed?: boolean
|
||||
}
|
||||
|
||||
export function AuthenticationBadge({ item, detailed = false }: AuthenticationBadgeProps) {
|
||||
const getAuthenticationLevel = () => {
|
||||
if (!item.authenticity.verified) return 'unverified'
|
||||
|
||||
// Based on research: different authentication levels
|
||||
const hasSerialNumber = item.authenticity.notes?.includes('serial')
|
||||
const hasExpertVerification = item.authenticity.notes?.includes('expert')
|
||||
const hasDocumentation = item.authenticity.notes?.includes('verified')
|
||||
|
||||
if (hasSerialNumber && hasExpertVerification && hasDocumentation) {
|
||||
return 'premium'
|
||||
} else if (hasExpertVerification || hasSerialNumber) {
|
||||
return 'verified'
|
||||
} else {
|
||||
return 'basic'
|
||||
}
|
||||
}
|
||||
|
||||
const authLevel = getAuthenticationLevel()
|
||||
|
||||
const getAuthConfig = () => {
|
||||
switch (authLevel) {
|
||||
case 'premium':
|
||||
return {
|
||||
color: '#2D6A6A',
|
||||
icon: '🏆',
|
||||
label: 'Premium Verified',
|
||||
description: 'Expert authenticated with full documentation'
|
||||
}
|
||||
case 'verified':
|
||||
return {
|
||||
color: '#C89C2B',
|
||||
icon: '✓',
|
||||
label: 'Verified Authentic',
|
||||
description: 'Authenticated by specialist'
|
||||
}
|
||||
case 'basic':
|
||||
return {
|
||||
color: '#8B7D6B',
|
||||
icon: '◐',
|
||||
label: 'Basic Check',
|
||||
description: 'Standard verification completed'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: '#7B2E2E',
|
||||
icon: '?',
|
||||
label: 'Unverified',
|
||||
description: 'Authentication pending'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = getAuthConfig()
|
||||
|
||||
if (!detailed) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
backgroundColor: config.color,
|
||||
color: '#F7F1E1',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em'
|
||||
}}
|
||||
title={config.description}
|
||||
>
|
||||
<span>{config.icon}</span>
|
||||
{authLevel !== 'unverified' && 'Auth'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
border: `2px solid ${config.color}`,
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
backgroundColor: config.color,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#F7F1E1',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{config.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
color: config.color,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em'
|
||||
}}>
|
||||
{config.label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '11px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{config.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.authenticity.notes && (
|
||||
<div style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '12px',
|
||||
color: '#1E1A17',
|
||||
fontStyle: 'italic',
|
||||
paddingLeft: '32px'
|
||||
}}>
|
||||
"{item.authenticity.notes}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
import React from 'react'
|
||||
import { BlogPost } from '@/data/blogPosts'
|
||||
|
||||
interface BlogPostCardProps {
|
||||
post: BlogPost
|
||||
onReadMore: (post: BlogPost) => void
|
||||
}
|
||||
|
||||
export function BlogPostCard({ post, onReadMore }: BlogPostCardProps) {
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<article style={{
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #8B7D6B',
|
||||
padding: '24px',
|
||||
marginBottom: '32px',
|
||||
position: 'relative',
|
||||
boxShadow: '4px 4px 0 rgba(139, 125, 107, 0.2)'
|
||||
}}>
|
||||
{/* Featured Badge */}
|
||||
{post.featured && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-12px',
|
||||
right: '24px',
|
||||
backgroundColor: '#C89C2B',
|
||||
color: '#F7F1E1',
|
||||
padding: '4px 12px',
|
||||
fontFamily: 'Pacifico, cursive',
|
||||
fontSize: '14px',
|
||||
transform: 'rotate(3deg)',
|
||||
boxShadow: '2px 2px 4px rgba(0,0,0,0.2)'
|
||||
}}>
|
||||
Featured
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header Image */}
|
||||
{(post.previewImage || post.images[0]) && (
|
||||
<div style={{
|
||||
marginBottom: '24px',
|
||||
marginLeft: '-24px',
|
||||
marginRight: '-24px',
|
||||
marginTop: '-24px',
|
||||
height: '300px',
|
||||
backgroundImage: `url('${post.previewImage || post.images[0]}')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundColor: 'white',
|
||||
borderBottom: '2px solid #8B7D6B'
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Category & Date */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
color: '#C89C2B',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{post.category}
|
||||
</span>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{formatDate(post.datePublished)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '28px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
marginBottom: '16px',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
{/* Excerpt */}
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.6',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
{post.excerpt}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
{post.tags.slice(0, 3).map(tag => (
|
||||
<span key={tag} style={{
|
||||
backgroundColor: '#F7F1E1',
|
||||
border: '1px solid #8B7D6B',
|
||||
padding: '4px 8px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Read More Button */}
|
||||
<button
|
||||
onClick={() => onReadMore(post)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: '2px solid #C89C2B',
|
||||
color: '#C89C2B',
|
||||
padding: '12px 24px',
|
||||
fontFamily: 'Staatliches, sans-serif',
|
||||
fontSize: '16px',
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#C89C2B'
|
||||
e.currentTarget.style.color = '#F7F1E1'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
e.currentTarget.style.color = '#C89C2B'
|
||||
}}
|
||||
>
|
||||
Read Full Article →
|
||||
</button>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
interface BlogPostModalProps {
|
||||
post: BlogPost
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function BlogPostModal({ post, onClose }: BlogPostModalProps) {
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
// Convert markdown-style formatting to HTML
|
||||
const formatContent = (content: string) => {
|
||||
return content
|
||||
.split('\n\n')
|
||||
.map((paragraph, index) => {
|
||||
// Handle headers
|
||||
if (paragraph.startsWith('## ')) {
|
||||
return (
|
||||
<h3 key={index} style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '24px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
marginTop: '32px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{paragraph.replace('## ', '')}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
if (paragraph.startsWith('### ')) {
|
||||
return (
|
||||
<h4 key={index} style={{
|
||||
fontFamily: 'Staatliches, sans-serif',
|
||||
fontSize: '20px',
|
||||
color: '#C89C2B',
|
||||
marginTop: '24px',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{paragraph.replace('### ', '')}
|
||||
</h4>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle lists
|
||||
if (paragraph.startsWith('- ')) {
|
||||
const items = paragraph.split('\n').filter(line => line.startsWith('- '))
|
||||
return (
|
||||
<ul key={index} style={{
|
||||
marginBottom: '16px',
|
||||
paddingLeft: '24px'
|
||||
}}>
|
||||
{items.map((item, i) => (
|
||||
<li key={i} style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.8',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
{item.replace('- ', '').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
// Regular paragraphs with bold text support
|
||||
const formattedText = paragraph.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
return (
|
||||
<p key={index} style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.8',
|
||||
marginBottom: '16px'
|
||||
}} dangerouslySetInnerHTML={{ __html: formattedText }} />
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(30, 26, 23, 0.8)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
overflowY: 'auto'
|
||||
}} onClick={onClose}>
|
||||
<article style={{
|
||||
backgroundColor: '#F7F1E1',
|
||||
maxWidth: '900px',
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto',
|
||||
padding: '48px',
|
||||
position: 'relative',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
fontSize: '32px',
|
||||
cursor: 'pointer',
|
||||
color: '#8B7D6B',
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '32px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
color: '#C89C2B',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{post.category}
|
||||
</span>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{formatDate(post.datePublished)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '42px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
lineHeight: '1.2',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<div className="newspaper-rule" style={{
|
||||
width: '100px',
|
||||
height: '2px',
|
||||
backgroundColor: '#C89C2B',
|
||||
margin: '24px 0'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Content with interleaved images */}
|
||||
<div style={{ marginBottom: '32px' }}>
|
||||
{(() => {
|
||||
const blocks = post.content.split('\n\n')
|
||||
const nodes: React.ReactNode[] = []
|
||||
const totalBlocks = blocks.length
|
||||
const totalImages = post.images.length
|
||||
const step = totalImages > 0 ? Math.ceil(totalBlocks / (totalImages + 1)) : Infinity
|
||||
let imageIndex = 0
|
||||
|
||||
blocks.forEach((paragraph, index) => {
|
||||
// Headers
|
||||
if (paragraph.startsWith('## ')) {
|
||||
nodes.push(
|
||||
<h3 key={`h3-${index}`} style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '24px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
marginTop: '32px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{paragraph.replace('## ', '')}
|
||||
</h3>
|
||||
)
|
||||
} else if (paragraph.startsWith('### ')) {
|
||||
nodes.push(
|
||||
<h4 key={`h4-${index}`} style={{
|
||||
fontFamily: 'Staatliches, sans-serif',
|
||||
fontSize: '20px',
|
||||
color: '#C89C2B',
|
||||
marginTop: '24px',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{paragraph.replace('### ', '')}
|
||||
</h4>
|
||||
)
|
||||
} else if (paragraph.startsWith('- ')) {
|
||||
const items = paragraph.split('\n').filter(line => line.startsWith('- '))
|
||||
nodes.push(
|
||||
<ul key={`ul-${index}`} style={{
|
||||
marginBottom: '16px',
|
||||
paddingLeft: '24px'
|
||||
}}>
|
||||
{items.map((item, i) => (
|
||||
<li
|
||||
key={`li-${index}-${i}`}
|
||||
style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.8',
|
||||
marginBottom: '8px'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.replace('- ', '').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
} else {
|
||||
// Paragraphs
|
||||
const formattedText = paragraph.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
nodes.push(
|
||||
<p key={`p-${index}`} style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.8',
|
||||
marginBottom: '16px'
|
||||
}} dangerouslySetInnerHTML={{ __html: formattedText }} />
|
||||
)
|
||||
}
|
||||
|
||||
// Interleave images evenly through the content
|
||||
if (totalImages > 0 && imageIndex < totalImages && (index + 1) % step === 0) {
|
||||
nodes.push(
|
||||
<div
|
||||
key={`img-${imageIndex}`}
|
||||
style={{
|
||||
margin: '16px 0 32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
border: '2px solid #8B7D6B',
|
||||
backgroundColor: 'white',
|
||||
padding: '6px',
|
||||
maxWidth: '360px',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={post.images[imageIndex]}
|
||||
alt={`Image ${imageIndex + 1}`}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
backgroundColor: '#F7F1E1'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
imageIndex++
|
||||
}
|
||||
})
|
||||
|
||||
// If any images remain, append them at the end
|
||||
while (imageIndex < totalImages) {
|
||||
nodes.push(
|
||||
<div
|
||||
key={`img-tail-${imageIndex}`}
|
||||
style={{
|
||||
margin: '16px 0 32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
border: '2px solid #8B7D6B',
|
||||
backgroundColor: 'white',
|
||||
padding: '6px',
|
||||
maxWidth: '360px',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={post.images[imageIndex]}
|
||||
alt={`Image ${imageIndex + 1}`}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
backgroundColor: '#F7F1E1'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
imageIndex++
|
||||
}
|
||||
|
||||
return nodes
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div style={{
|
||||
borderTop: '2px solid #8B7D6B',
|
||||
paddingTop: '24px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{post.tags.map(tag => (
|
||||
<span key={tag} style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #8B7D6B',
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ClientOnlyProps {
|
||||
children: React.ReactNode
|
||||
fallback?: React.ReactNode
|
||||
}
|
||||
|
||||
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
|
||||
const [hasMounted, setHasMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setHasMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!hasMounted) {
|
||||
return <>{fallback}</>
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
'use client'
|
||||
|
||||
import { useApp } from '@/contexts/AppContext'
|
||||
import { CuratedItem } from '@/types'
|
||||
|
||||
interface CollectorMotivationProps {
|
||||
item: CuratedItem
|
||||
}
|
||||
|
||||
export function CollectorMotivation({ item }: CollectorMotivationProps) {
|
||||
const { state } = useApp()
|
||||
|
||||
// Based on research: collector psychology and motivation triggers
|
||||
const getMotivationTriggers = () => {
|
||||
const triggers = []
|
||||
|
||||
// Scarcity trigger
|
||||
if (item.rarity >= 4) {
|
||||
triggers.push({
|
||||
type: 'scarcity',
|
||||
icon: '⚡',
|
||||
title: 'Rare Find',
|
||||
message: 'Only a few of these exist in this condition',
|
||||
color: '#7B2E2E'
|
||||
})
|
||||
}
|
||||
|
||||
// Heritage/History trigger
|
||||
if (item.dateAdded && new Date(item.dateAdded).getFullYear() < 1980) {
|
||||
triggers.push({
|
||||
type: 'heritage',
|
||||
icon: '🏛️',
|
||||
title: 'Historical Significance',
|
||||
message: 'Connects you to a rich cultural heritage',
|
||||
color: '#2D6A6A'
|
||||
})
|
||||
}
|
||||
|
||||
// Investment trigger
|
||||
if (item.originalPrice && item.price < item.originalPrice * 0.8) {
|
||||
triggers.push({
|
||||
type: 'investment',
|
||||
icon: '💎',
|
||||
title: 'Investment Potential',
|
||||
message: 'Undervalued compared to market trends',
|
||||
color: '#C89C2B'
|
||||
})
|
||||
}
|
||||
|
||||
// Completion trigger (if user has similar items)
|
||||
const userCategoryItems = state.wishlist.filter(id => {
|
||||
const wishlistItem = state.items.find(i => i.id === id)
|
||||
return wishlistItem?.category === item.category
|
||||
})
|
||||
|
||||
if (userCategoryItems.length >= 2) {
|
||||
triggers.push({
|
||||
type: 'completion',
|
||||
icon: '🧩',
|
||||
title: 'Collection Builder',
|
||||
message: `Perfect addition to your ${item.category} collection`,
|
||||
color: '#4A7C59'
|
||||
})
|
||||
}
|
||||
|
||||
// Authenticity trust trigger
|
||||
if (item.authenticity.verified) {
|
||||
triggers.push({
|
||||
type: 'trust',
|
||||
icon: '🛡️',
|
||||
title: 'Verified Authentic',
|
||||
message: 'Buy with confidence - authenticity guaranteed',
|
||||
color: '#2D6A6A'
|
||||
})
|
||||
}
|
||||
|
||||
return triggers.slice(0, 3) // Show max 3 triggers
|
||||
}
|
||||
|
||||
const getCollectorPersonality = () => {
|
||||
const categoryCount = state.wishlist.reduce((acc, id) => {
|
||||
const item = state.items.find(i => i.id === id)
|
||||
if (item) {
|
||||
acc[item.category] = (acc[item.category] || 0) + 1
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
const dominantCategory = Object.entries(categoryCount)
|
||||
.sort(([,a], [,b]) => b - a)[0]?.[0]
|
||||
|
||||
const personalities = {
|
||||
photography: {
|
||||
type: 'Visual Historian',
|
||||
description: 'You preserve moments and appreciate craftsmanship',
|
||||
motivation: 'Each camera tells a story of innovation'
|
||||
},
|
||||
music: {
|
||||
type: 'Audio Archivist',
|
||||
description: 'You value authentic sound and musical heritage',
|
||||
motivation: 'Original pressings capture the artist\'s true vision'
|
||||
},
|
||||
books: {
|
||||
type: 'Literary Curator',
|
||||
description: 'You collect knowledge and cultural significance',
|
||||
motivation: 'First editions connect you to literary history'
|
||||
},
|
||||
design: {
|
||||
type: 'Aesthetic Connoisseur',
|
||||
description: 'You appreciate timeless design and functionality',
|
||||
motivation: 'Mid-century pieces represent design perfection'
|
||||
}
|
||||
}
|
||||
|
||||
return personalities[dominantCategory as keyof typeof personalities] || {
|
||||
type: 'Eclectic Collector',
|
||||
description: 'You appreciate quality across all categories',
|
||||
motivation: 'Diverse collections tell richer stories'
|
||||
}
|
||||
}
|
||||
|
||||
const triggers = getMotivationTriggers()
|
||||
const personality = getCollectorPersonality()
|
||||
|
||||
if (triggers.length === 0) return null
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: 'rgba(123, 46, 46, 0.05)',
|
||||
border: '1px solid rgba(123, 46, 46, 0.2)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{/* Collector Personality */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '12px',
|
||||
paddingBottom: '8px',
|
||||
borderBottom: '1px solid rgba(123, 46, 46, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
backgroundColor: '#7B2E2E',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
🎯
|
||||
</div>
|
||||
<div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#7B2E2E',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{personality.type}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '10px',
|
||||
color: '#8B7D6B',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{personality.motivation}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Motivation Triggers */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{triggers.map((trigger, index) => (
|
||||
<div
|
||||
key={trigger.type}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.5)',
|
||||
borderRadius: '4px',
|
||||
borderLeft: `3px solid ${trigger.color}`
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '14px' }}>{trigger.icon}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: trigger.color,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2px'
|
||||
}}>
|
||||
{trigger.title}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '11px',
|
||||
color: '#1E1A17'
|
||||
}}>
|
||||
{trigger.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Call to Action */}
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px',
|
||||
backgroundColor: 'rgba(123, 46, 46, 0.1)',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'Pacifico, cursive',
|
||||
fontSize: '12px',
|
||||
color: '#7B2E2E',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
Why collectors love this piece
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '11px',
|
||||
color: '#1E1A17',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
"{personality.description}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function FloatingElements() {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setMousePosition({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
// Add a small delay to ensure proper hydration
|
||||
const timer = setTimeout(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Don't render on server side or immediately after mount
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
const floatingItems = [
|
||||
{ icon: '📸', size: 24, speed: 0.02, offset: { x: 100, y: 50 } },
|
||||
{ icon: '🎵', size: 20, speed: 0.03, offset: { x: -80, y: 100 } },
|
||||
{ icon: '📚', size: 22, speed: 0.025, offset: { x: 150, y: -30 } },
|
||||
{ icon: '🎨', size: 26, speed: 0.015, offset: { x: -120, y: -80 } },
|
||||
{ icon: '⭐', size: 18, speed: 0.04, offset: { x: 200, y: 120 } },
|
||||
{ icon: '💎', size: 16, speed: 0.035, offset: { x: -200, y: 60 } }
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: -1 }}>
|
||||
{floatingItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: `${item.size}px`,
|
||||
opacity: 0.1,
|
||||
transform: `translate3d(${mousePosition.x * item.speed + item.offset.x}px, ${mousePosition.y * item.speed + item.offset.y}px, 0)`,
|
||||
transition: 'transform 0.5s ease-out',
|
||||
animation: `float-${index} ${3 + index * 0.5}s ease-in-out infinite`,
|
||||
left: `${20 + (index * 15)}%`,
|
||||
top: `${10 + (index * 12)}%`
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<style jsx>{`
|
||||
${floatingItems.map((_, index) => `
|
||||
@keyframes float-${index} {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-${10 + index * 2}px) rotate(${index % 2 === 0 ? 2 : -2}deg); }
|
||||
}
|
||||
`).join('')}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { CuratedItem } from '@/types'
|
||||
import { useApp } from '@/contexts/AppContext'
|
||||
import { AuthenticationBadge } from './AuthenticationBadge'
|
||||
import { ItemDetailModal } from './ItemDetailModal'
|
||||
|
||||
interface ItemCardProps {
|
||||
item: CuratedItem
|
||||
showWishlist?: boolean
|
||||
showExpertNotes?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export function ItemCard({
|
||||
item,
|
||||
showWishlist = true,
|
||||
showExpertNotes = true,
|
||||
compact = false
|
||||
}: ItemCardProps) {
|
||||
const { state, toggleWishlist, addToRecentlyViewed } = useApp()
|
||||
const [showDetailModal, setShowDetailModal] = useState(false)
|
||||
const isWishlisted = state.wishlist.includes(item.id)
|
||||
|
||||
const handleItemClick = () => {
|
||||
addToRecentlyViewed(item.id)
|
||||
setShowDetailModal(true)
|
||||
}
|
||||
|
||||
const handleWishlistClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
toggleWishlist(item.id)
|
||||
}
|
||||
|
||||
const getRarityStars = (rarity: number) => {
|
||||
return '★'.repeat(rarity) + '☆'.repeat(5 - rarity)
|
||||
}
|
||||
|
||||
const getConditionColor = (condition: string) => {
|
||||
switch (condition) {
|
||||
case 'new': return '#2D6A6A'
|
||||
case 'like-new': return '#4A7C59'
|
||||
case 'excellent': return '#C89C2B'
|
||||
case 'very-good': return '#D4A574'
|
||||
case 'good': return '#8B7D6B'
|
||||
case 'acceptable': return '#7B2E2E'
|
||||
default: return '#8B7D6B'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pick-card"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
height: compact ? 'auto' : '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
transformStyle: 'preserve-3d'
|
||||
}}
|
||||
onClick={handleItemClick}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-8px) rotateX(2deg)'
|
||||
e.currentTarget.style.boxShadow = '0 20px 40px rgba(30, 26, 23, 0.15)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0px) rotateX(0deg)'
|
||||
e.currentTarget.style.boxShadow = '0 2px 4px rgba(30, 26, 23, 0.1)'
|
||||
}}
|
||||
>
|
||||
{/* Wishlist Button */}
|
||||
{showWishlist && (
|
||||
<button
|
||||
onClick={handleWishlistClick}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
right: '12px',
|
||||
zIndex: 10,
|
||||
background: isWishlisted ? '#7B2E2E' : 'rgba(255, 255, 255, 0.9)',
|
||||
color: isWishlisted ? '#F7F1E1' : '#7B2E2E',
|
||||
border: '2px solid #7B2E2E',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '18px',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
title={isWishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
|
||||
>
|
||||
{isWishlisted ? '❤️' : '🤍'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Featured Badge */}
|
||||
{item.featured && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
left: '12px',
|
||||
zIndex: 10,
|
||||
background: '#C89C2B',
|
||||
color: '#F7F1E1',
|
||||
padding: '4px 12px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '0 8px 8px 0'
|
||||
}}>
|
||||
Featured
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category Tab */}
|
||||
<div className="folder-tab">
|
||||
<div className="folder-tab-label" style={{ textTransform: 'capitalize' }}>
|
||||
{item.category}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: compact ? '150px' : '200px',
|
||||
backgroundImage: `url("${item.images[0]}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
border: '2px solid #8B7D6B',
|
||||
margin: '0 16px 16px 16px',
|
||||
borderRadius: '4px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Price Overlay */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
left: '8px',
|
||||
background: 'rgba(30, 26, 23, 0.9)',
|
||||
color: '#F7F1E1',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
${item.price}
|
||||
{item.originalPrice && (
|
||||
<span style={{
|
||||
textDecoration: 'line-through',
|
||||
opacity: 0.7,
|
||||
marginLeft: '8px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
${item.originalPrice}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authenticity Badge */}
|
||||
{item.authenticity.verified && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '8px',
|
||||
background: '#2D6A6A',
|
||||
color: '#F7F1E1',
|
||||
borderRadius: '50%',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
✓
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{
|
||||
padding: '0 16px 16px 16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
flex: 1
|
||||
}}>
|
||||
{/* Title */}
|
||||
<h3 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: compact ? '18px' : '20px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
lineHeight: '1.2',
|
||||
margin: 0,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: compact ? 2 : 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
{/* Meta Info */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
{/* Condition */}
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
backgroundColor: getConditionColor(item.condition),
|
||||
color: '#F7F1E1',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
{item.condition}
|
||||
</span>
|
||||
|
||||
{/* Rarity */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<span style={{ color: '#C89C2B', fontSize: '14px' }}>
|
||||
{getRarityStars(item.rarity)}
|
||||
</span>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
color: '#8B7D6B',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em'
|
||||
}}>
|
||||
Rarity {item.rarity}/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expert Notes */}
|
||||
{showExpertNotes && !compact && (
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#1E1A17',
|
||||
margin: 0,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{item.expertNotes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6px',
|
||||
marginTop: 'auto'
|
||||
}}>
|
||||
{item.tags.slice(0, compact ? 2 : 3).map(tag => (
|
||||
<span key={tag} style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
backgroundColor: 'rgba(139, 125, 107, 0.1)',
|
||||
border: '1px solid #8B7D6B',
|
||||
color: '#8B7D6B',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{item.tags.length > (compact ? 2 : 3) && (
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
+{item.tags.length - (compact ? 2 : 3)} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Seller Info */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: '8px',
|
||||
borderTop: '1px solid rgba(139, 125, 107, 0.2)',
|
||||
fontSize: '12px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
<div>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace' }}>
|
||||
{item.seller.name}
|
||||
</span>
|
||||
<span style={{ marginLeft: '8px' }}>
|
||||
⭐ {item.seller.rating} ({item.seller.feedback})
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace' }}>
|
||||
+${item.shipping.cost} shipping
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{!compact && (
|
||||
<div style={{ display: 'flex', gap: '8px', paddingTop: '8px' }}>
|
||||
<button
|
||||
className="ticket-button"
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: '10px',
|
||||
padding: '8px 12px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleItemClick()
|
||||
}}
|
||||
>
|
||||
View on eBay
|
||||
</button>
|
||||
<button
|
||||
className="stamp-button"
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '8px 12px'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// In real app, this would show more details
|
||||
alert('Item details coming soon!')
|
||||
}}
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail Modal */}
|
||||
<ItemDetailModal
|
||||
item={item}
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,439 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { CuratedItem } from '@/types'
|
||||
import { useApp } from '@/contexts/AppContext'
|
||||
import { AuthenticationBadge } from './AuthenticationBadge'
|
||||
import { MarketInsights } from './MarketInsights'
|
||||
import { CollectorMotivation } from './CollectorMotivation'
|
||||
|
||||
interface ItemDetailModalProps {
|
||||
item: CuratedItem
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function ItemDetailModal({ item, isOpen, onClose }: ItemDetailModalProps) {
|
||||
const { state, toggleWishlist } = useApp()
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const isWishlisted = state.wishlist.includes(item.id)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleWishlistClick = () => {
|
||||
toggleWishlist(item.id)
|
||||
}
|
||||
|
||||
const handleEbayClick = () => {
|
||||
window.open(item.ebayUrl, '_blank')
|
||||
}
|
||||
|
||||
const getRarityStars = (rarity: number) => {
|
||||
return '★'.repeat(rarity) + '☆'.repeat(5 - rarity)
|
||||
}
|
||||
|
||||
const getConditionColor = (condition: string) => {
|
||||
switch (condition) {
|
||||
case 'new': return '#2D6A6A'
|
||||
case 'like-new': return '#4A7C59'
|
||||
case 'excellent': return '#C89C2B'
|
||||
case 'very-good': return '#D4A574'
|
||||
case 'good': return '#8B7D6B'
|
||||
case 'acceptable': return '#7B2E2E'
|
||||
default: return '#8B7D6B'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(30, 26, 23, 0.8)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px'
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#F7F1E1',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '900px',
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
boxShadow: '0 20px 40px rgba(30, 26, 23, 0.3)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
right: '16px',
|
||||
background: 'rgba(30, 26, 23, 0.1)',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '18px',
|
||||
color: '#1E1A17',
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '32px', padding: '32px' }}>
|
||||
{/* Left Column - Images */}
|
||||
<div>
|
||||
{/* Main Image */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '300px',
|
||||
backgroundImage: `url("${item.images[currentImageIndex]}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
border: '2px solid #8B7D6B',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '16px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Image Navigation */}
|
||||
{item.images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setCurrentImageIndex(Math.max(0, currentImageIndex - 1))}
|
||||
disabled={currentImageIndex === 0}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'rgba(30, 26, 23, 0.7)',
|
||||
color: '#F7F1E1',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
cursor: currentImageIndex === 0 ? 'not-allowed' : 'pointer',
|
||||
opacity: currentImageIndex === 0 ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentImageIndex(Math.min(item.images.length - 1, currentImageIndex + 1))}
|
||||
disabled={currentImageIndex === item.images.length - 1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'rgba(30, 26, 23, 0.7)',
|
||||
color: '#F7F1E1',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
cursor: currentImageIndex === item.images.length - 1 ? 'not-allowed' : 'pointer',
|
||||
opacity: currentImageIndex === item.images.length - 1 ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Images */}
|
||||
{item.images.length > 1 && (
|
||||
<div style={{ display: 'flex', gap: '8px', overflowX: 'auto' }}>
|
||||
{item.images.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentImageIndex(index)}
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
backgroundImage: `url("${image}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
border: index === currentImageIndex ? '2px solid #C89C2B' : '2px solid #8B7D6B',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authentication Badge */}
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<AuthenticationBadge item={item} detailed={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Details */}
|
||||
<div>
|
||||
{/* Category Badge */}
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#C89C2B',
|
||||
color: '#F7F1E1',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{item.category}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '32px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
lineHeight: '1.2',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{item.title}
|
||||
</h1>
|
||||
|
||||
{/* Price */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '28px',
|
||||
color: '#1E1A17'
|
||||
}}>
|
||||
${item.price}
|
||||
</span>
|
||||
{item.originalPrice && (
|
||||
<span style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '18px',
|
||||
color: '#8B7D6B',
|
||||
textDecoration: 'line-through'
|
||||
}}>
|
||||
${item.originalPrice}
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
+ ${item.shipping.cost} shipping
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Condition & Rarity */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
backgroundColor: getConditionColor(item.condition),
|
||||
color: '#F7F1E1',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
{item.condition}
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<span style={{ color: '#C89C2B', fontSize: '16px' }}>
|
||||
{getRarityStars(item.rarity)}
|
||||
</span>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
color: '#8B7D6B',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em'
|
||||
}}>
|
||||
Rarity {item.rarity}/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Market Insights */}
|
||||
<MarketInsights item={item} />
|
||||
|
||||
{/* Collector Motivation */}
|
||||
<CollectorMotivation item={item} />
|
||||
|
||||
{/* Expert Notes */}
|
||||
<div style={{
|
||||
backgroundColor: 'rgba(139, 125, 107, 0.1)',
|
||||
border: '1px solid rgba(139, 125, 107, 0.2)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<h4 style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Expert Notes
|
||||
</h4>
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6',
|
||||
color: '#1E1A17',
|
||||
margin: 0,
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
"{item.expertNotes}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h4 style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Tags
|
||||
</h4>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{item.tags.map(tag => (
|
||||
<span key={tag} style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
backgroundColor: 'rgba(139, 125, 107, 0.1)',
|
||||
border: '1px solid #8B7D6B',
|
||||
color: '#8B7D6B',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={handleEbayClick}
|
||||
className="ticket-button"
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: '14px',
|
||||
padding: '16px 24px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
View on eBay
|
||||
</button>
|
||||
<button
|
||||
onClick={handleWishlistClick}
|
||||
style={{
|
||||
padding: '16px',
|
||||
backgroundColor: isWishlisted ? '#7B2E2E' : 'transparent',
|
||||
color: isWishlisted ? '#F7F1E1' : '#7B2E2E',
|
||||
border: '2px solid #7B2E2E',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
title={isWishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
|
||||
>
|
||||
{isWishlisted ? '❤️' : '🤍'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Seller Info */}
|
||||
<div style={{
|
||||
marginTop: '24px',
|
||||
paddingTop: '16px',
|
||||
borderTop: '1px solid rgba(139, 125, 107, 0.2)'
|
||||
}}>
|
||||
<h4 style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Seller Information
|
||||
</h4>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#1E1A17'
|
||||
}}>
|
||||
<div>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontWeight: 'bold' }}>
|
||||
{item.seller.name}
|
||||
</span>
|
||||
<div style={{ fontSize: '12px', color: '#8B7D6B' }}>
|
||||
⭐ {item.seller.rating} ({item.seller.feedback} reviews)
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: '12px' }}>
|
||||
Ships from: {item.shipping.location}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#8B7D6B' }}>
|
||||
{item.shipping.international ? 'International shipping' : 'Domestic only'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
'use client'
|
||||
|
||||
import { CuratedItem } from '@/types'
|
||||
|
||||
interface MarketInsightsProps {
|
||||
item: CuratedItem
|
||||
}
|
||||
|
||||
export function MarketInsights({ item }: MarketInsightsProps) {
|
||||
// Based on research: collectibles market growing at 5.75% CAGR
|
||||
const getMarketTrend = () => {
|
||||
const categoryTrends = {
|
||||
photography: { trend: 'up', percentage: 12, reason: 'Film photography revival' },
|
||||
music: { trend: 'up', percentage: 8, reason: 'Vinyl resurgence continues' },
|
||||
books: { trend: 'stable', percentage: 3, reason: 'Steady collector interest' },
|
||||
design: { trend: 'up', percentage: 15, reason: 'Mid-century modern demand' }
|
||||
}
|
||||
|
||||
return categoryTrends[item.category] || { trend: 'stable', percentage: 5, reason: 'Market average' }
|
||||
}
|
||||
|
||||
const getPriceAnalysis = () => {
|
||||
if (!item.originalPrice) return null
|
||||
|
||||
const discount = Math.round(((item.originalPrice - item.price) / item.originalPrice) * 100)
|
||||
const isGoodDeal = discount > 15
|
||||
|
||||
return { discount, isGoodDeal }
|
||||
}
|
||||
|
||||
const getInvestmentScore = () => {
|
||||
let score = 5 // Base score
|
||||
|
||||
// Rarity bonus
|
||||
score += item.rarity * 1.5
|
||||
|
||||
// Authentication bonus
|
||||
if (item.authenticity.verified) score += 2
|
||||
|
||||
// Category trend bonus
|
||||
const trend = getMarketTrend()
|
||||
if (trend.trend === 'up') score += 1.5
|
||||
|
||||
// Condition bonus
|
||||
const conditionBonus = {
|
||||
'new': 2,
|
||||
'like-new': 1.5,
|
||||
'excellent': 1,
|
||||
'very-good': 0.5,
|
||||
'good': 0,
|
||||
'acceptable': -0.5
|
||||
}
|
||||
score += conditionBonus[item.condition] || 0
|
||||
|
||||
return Math.min(Math.max(Math.round(score), 1), 10)
|
||||
}
|
||||
|
||||
const trend = getMarketTrend()
|
||||
const priceAnalysis = getPriceAnalysis()
|
||||
const investmentScore = getInvestmentScore()
|
||||
|
||||
const getTrendIcon = (trendType: string) => {
|
||||
switch (trendType) {
|
||||
case 'up': return '📈'
|
||||
case 'down': return '📉'
|
||||
default: return '➡️'
|
||||
}
|
||||
}
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 8) return '#2D6A6A'
|
||||
if (score >= 6) return '#C89C2B'
|
||||
if (score >= 4) return '#8B7D6B'
|
||||
return '#7B2E2E'
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: 'rgba(45, 106, 106, 0.05)',
|
||||
border: '1px solid rgba(45, 106, 106, 0.2)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
backgroundColor: '#2D6A6A',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
📊
|
||||
</div>
|
||||
<h4 style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#2D6A6A',
|
||||
margin: 0,
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Market Intelligence
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
|
||||
gap: '12px'
|
||||
}}>
|
||||
{/* Investment Score */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
color: getScoreColor(investmentScore),
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{investmentScore}/10
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
Investment Score
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Market Trend */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<span>{getTrendIcon(trend.trend)}</span>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: trend.trend === 'up' ? '#2D6A6A' : trend.trend === 'down' ? '#7B2E2E' : '#8B7D6B'
|
||||
}}>
|
||||
+{trend.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
Category Trend
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Analysis */}
|
||||
{priceAnalysis && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: priceAnalysis.isGoodDeal ? '#2D6A6A' : '#8B7D6B',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
-{priceAnalysis.discount}%
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{priceAnalysis.isGoodDeal ? 'Great Deal' : 'Fair Price'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trend Reason */}
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.5)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'Spectral, serif',
|
||||
color: '#1E1A17',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
💡 {trend.reason}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface RevealAnimationProps {
|
||||
children: React.ReactNode
|
||||
direction?: 'up' | 'down' | 'left' | 'right' | 'fade' | 'scale'
|
||||
delay?: number
|
||||
duration?: number
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function RevealAnimation({
|
||||
children,
|
||||
direction = 'up',
|
||||
delay = 0,
|
||||
duration = 0.6,
|
||||
className = '',
|
||||
style = {}
|
||||
}: RevealAnimationProps) {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
setIsVisible(true)
|
||||
}, delay * 1000)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '50px' }
|
||||
)
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [delay])
|
||||
|
||||
const getTransform = () => {
|
||||
if (isVisible) return 'translate3d(0, 0, 0) scale(1)'
|
||||
|
||||
switch (direction) {
|
||||
case 'up': return 'translate3d(0, 50px, 0) scale(1)'
|
||||
case 'down': return 'translate3d(0, -50px, 0) scale(1)'
|
||||
case 'left': return 'translate3d(50px, 0, 0) scale(1)'
|
||||
case 'right': return 'translate3d(-50px, 0, 0) scale(1)'
|
||||
case 'scale': return 'translate3d(0, 0, 0) scale(0.8)'
|
||||
default: return 'translate3d(0, 50px, 0) scale(1)'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transform: getTransform(),
|
||||
transition: `all ${duration}s cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
|
||||
willChange: 'transform, opacity',
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function ScrollEffects() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [scrollProgress, setScrollProgress] = useState(0)
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY
|
||||
setScrollY(currentScrollY)
|
||||
setIsVisible(currentScrollY > 100)
|
||||
|
||||
// Calculate scroll progress safely
|
||||
const documentHeight = document.documentElement.scrollHeight
|
||||
const windowHeight = window.innerHeight
|
||||
const scrollableHeight = documentHeight - windowHeight
|
||||
|
||||
if (scrollableHeight > 0) {
|
||||
const progress = Math.min((currentScrollY / scrollableHeight) * 100, 100)
|
||||
setScrollProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
// Only add event listener on client side
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
// Initial calculation
|
||||
handleScroll()
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const scrollToTop = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
|
||||
// Don't render until mounted on client
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Back to Top Button */}
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '24px',
|
||||
right: '24px',
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #C89C2B, #7B2E2E)',
|
||||
color: '#F7F1E1',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
zIndex: 1000,
|
||||
boxShadow: '0 4px 12px rgba(30, 26, 23, 0.3)',
|
||||
transform: isVisible ? 'translateY(0) scale(1)' : 'translateY(100px) scale(0)',
|
||||
transition: 'all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="Back to top"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
|
||||
{/* Scroll Progress Bar */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `${scrollProgress}%`,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #C89C2B, #7B2E2E)',
|
||||
zIndex: 1001,
|
||||
transition: 'width 0.1s ease'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Floating Navigation Dots */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: '24px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ id: 'hero', label: 'Top' },
|
||||
{ id: 'latest-picks', label: 'Items' },
|
||||
{ id: 'newsletter', label: 'Newsletter' }
|
||||
].map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => {
|
||||
const element = document.getElementById(section.id)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #C89C2B',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative'
|
||||
}}
|
||||
title={section.label}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#C89C2B'
|
||||
e.currentTarget.style.transform = 'scale(1.2)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.transform = 'scale(1)'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useApp } from '@/contexts/AppContext'
|
||||
|
||||
interface SearchBarProps {
|
||||
placeholder?: string
|
||||
showFilters?: boolean
|
||||
}
|
||||
|
||||
export function SearchBar({ placeholder = "Search vintage treasures...", showFilters = true }: SearchBarProps) {
|
||||
const { state, searchItems } = useApp()
|
||||
const [query, setQuery] = useState(state.searchQuery)
|
||||
const [showFilterPanel, setShowFilterPanel] = useState(false)
|
||||
const [filters, setFilters] = useState(state.searchFilters)
|
||||
|
||||
useEffect(() => {
|
||||
const debounceTimer = setTimeout(() => {
|
||||
searchItems(query, filters)
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(debounceTimer)
|
||||
}, [query, filters])
|
||||
|
||||
const handleFilterChange = (key: string, value: any) => {
|
||||
const newFilters = { ...filters, [key]: value }
|
||||
setFilters(newFilters)
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({})
|
||||
setQuery('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="search-container" style={{ position: 'relative', width: '100%', maxWidth: '600px' }}>
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #8B7D6B',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
color: '#8B7D6B',
|
||||
fontSize: '18px'
|
||||
}}>
|
||||
🔍
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 0',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#1E1A17',
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
/>
|
||||
{showFilters && (
|
||||
<button
|
||||
onClick={() => setShowFilterPanel(!showFilterPanel)}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
border: 'none',
|
||||
backgroundColor: showFilterPanel ? '#C89C2B' : 'transparent',
|
||||
color: showFilterPanel ? '#F7F1E1' : '#8B7D6B',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter Panel */}
|
||||
{showFilterPanel && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #8B7D6B',
|
||||
borderTop: 'none',
|
||||
padding: '24px',
|
||||
zIndex: 10,
|
||||
boxShadow: '0 4px 8px rgba(30, 26, 23, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '24px'
|
||||
}}>
|
||||
{/* Category Filter */}
|
||||
<div>
|
||||
<label style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B',
|
||||
display: 'block',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
value={filters.category || ''}
|
||||
onChange={(e) => handleFilterChange('category', e.target.value || undefined)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #8B7D6B',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
<option value="photography">Photography</option>
|
||||
<option value="music">Music</option>
|
||||
<option value="books">Books</option>
|
||||
<option value="design">Design</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Price Range */}
|
||||
<div>
|
||||
<label style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B',
|
||||
display: 'block',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Price Range
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={filters.priceMin || ''}
|
||||
onChange={(e) => handleFilterChange('priceMin', e.target.value ? Number(e.target.value) : undefined)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #8B7D6B',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#8B7D6B' }}>-</span>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={filters.priceMax || ''}
|
||||
onChange={(e) => handleFilterChange('priceMax', e.target.value ? Number(e.target.value) : undefined)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #8B7D6B',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort By */}
|
||||
<div>
|
||||
<label style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#8B7D6B',
|
||||
display: 'block',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Sort By
|
||||
</label>
|
||||
<select
|
||||
value={filters.sortBy || 'date'}
|
||||
onChange={(e) => handleFilterChange('sortBy', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #8B7D6B',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<option value="date">Newest First</option>
|
||||
<option value="price-low">Price: Low to High</option>
|
||||
<option value="price-high">Price: High to Low</option>
|
||||
<option value="rarity">Rarity</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Actions */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '24px',
|
||||
paddingTop: '16px',
|
||||
borderTop: '1px solid #8B7D6B'
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
color: '#8B7D6B'
|
||||
}}>
|
||||
{state.filteredItems.length} items found
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #8B7D6B',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#8B7D6B',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowFilterPanel(false)}
|
||||
className="ticket-button"
|
||||
style={{ fontSize: '12px', padding: '8px 16px' }}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
'use client'
|
||||
|
||||
import { useApp } from '@/contexts/AppContext'
|
||||
import { Badge } from '@/types'
|
||||
|
||||
export function UserProfile() {
|
||||
const { state } = useApp()
|
||||
const { user } = state
|
||||
|
||||
if (!user) return null
|
||||
|
||||
const getXPProgress = () => {
|
||||
const xpForNextLevel = user.level * 100 // Simple formula: level * 100 XP needed
|
||||
const currentLevelXP = (user.level - 1) * 100
|
||||
const progressXP = user.xp - currentLevelXP
|
||||
const neededXP = xpForNextLevel - currentLevelXP
|
||||
return {
|
||||
progress: progressXP,
|
||||
needed: neededXP,
|
||||
percentage: (progressXP / neededXP) * 100
|
||||
}
|
||||
}
|
||||
|
||||
const getBadgeRarityColor = (rarity: Badge['rarity']) => {
|
||||
switch (rarity) {
|
||||
case 'common': return '#8B7D6B'
|
||||
case 'rare': return '#C89C2B'
|
||||
case 'legendary': return '#7B2E2E'
|
||||
default: return '#8B7D6B'
|
||||
}
|
||||
}
|
||||
|
||||
const xpProgress = getXPProgress()
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #233043 0%, #2D6A6A 100%)',
|
||||
color: '#F7F1E1',
|
||||
padding: '24px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Background Pattern */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundImage: 'radial-gradient(circle at 20% 80%, rgba(200, 156, 43, 0.1) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(123, 46, 46, 0.1) 0%, transparent 50%)',
|
||||
pointerEvents: 'none'
|
||||
}} />
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '24px' }}>
|
||||
{/* Avatar */}
|
||||
<div style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #C89C2B, #7B2E2E)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#F7F1E1',
|
||||
border: '3px solid rgba(247, 241, 225, 0.2)'
|
||||
}}>
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* User Info */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '24px',
|
||||
margin: '0 0 4px 0',
|
||||
color: '#F7F1E1'
|
||||
}}>
|
||||
{user.username}
|
||||
</h3>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: 'rgba(247, 241, 225, 0.7)'
|
||||
}}>
|
||||
Level {user.level} Collector • {user.xp} XP
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Streak */}
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(247, 241, 225, 0.1)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(247, 241, 225, 0.2)'
|
||||
}}>
|
||||
<div style={{ fontSize: '20px', marginBottom: '4px' }}>🔥</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em'
|
||||
}}>
|
||||
{user.streak} Day Streak
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* XP Progress Bar */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: 'rgba(247, 241, 225, 0.7)'
|
||||
}}>
|
||||
Progress to Level {user.level + 1}
|
||||
</span>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
color: '#C89C2B'
|
||||
}}>
|
||||
{xpProgress.progress}/{xpProgress.needed} XP
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '8px',
|
||||
backgroundColor: 'rgba(247, 241, 225, 0.1)',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${Math.min(xpProgress.percentage, 100)}%`,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #C89C2B, #7B2E2E)',
|
||||
borderRadius: '4px',
|
||||
transition: 'width 0.3s ease'
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: '16px',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px',
|
||||
background: 'rgba(247, 241, 225, 0.05)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(247, 241, 225, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#C89C2B',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{user.wishlist.length}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: 'rgba(247, 241, 225, 0.7)'
|
||||
}}>
|
||||
Wishlisted
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px',
|
||||
background: 'rgba(247, 241, 225, 0.05)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(247, 241, 225, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#C89C2B',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{user.badges.length}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: 'rgba(247, 241, 225, 0.7)'
|
||||
}}>
|
||||
Badges
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px',
|
||||
background: 'rgba(247, 241, 225, 0.05)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(247, 241, 225, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#C89C2B',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{user.collections.length}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: 'rgba(247, 241, 225, 0.7)'
|
||||
}}>
|
||||
Collections
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
{user.badges.length > 0 && (
|
||||
<div>
|
||||
<h4 style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '14px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#C89C2B',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
Recent Badges
|
||||
</h4>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{user.badges.slice(0, 4).map((badge) => (
|
||||
<div
|
||||
key={badge.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(247, 241, 225, 0.1)',
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${getBadgeRarityColor(badge.rarity)}`,
|
||||
position: 'relative'
|
||||
}}
|
||||
title={badge.description}
|
||||
>
|
||||
<span style={{ fontSize: '16px' }}>{badge.icon}</span>
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: getBadgeRarityColor(badge.rarity)
|
||||
}}>
|
||||
{badge.name}
|
||||
</span>
|
||||
{badge.rarity === 'legendary' && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-4px',
|
||||
right: '-4px',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#7B2E2E',
|
||||
animation: 'pulse 2s infinite'
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
marginTop: '20px',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid rgba(247, 241, 225, 0.1)'
|
||||
}}>
|
||||
<button
|
||||
className="stamp-button"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: 'rgba(247, 241, 225, 0.1)',
|
||||
borderColor: 'rgba(247, 241, 225, 0.3)',
|
||||
color: '#F7F1E1'
|
||||
}}
|
||||
>
|
||||
View Wishlist
|
||||
</button>
|
||||
<button
|
||||
className="stamp-button"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: 'rgba(247, 241, 225, 0.1)',
|
||||
borderColor: 'rgba(247, 241, 225, 0.3)',
|
||||
color: '#F7F1E1'
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CSS Animation for legendary badge pulse */}
|
||||
<style jsx>{`
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useReducer, useEffect } from 'react'
|
||||
import { CuratedItem, UserProfile, SearchFilters, Badge } from '@/types'
|
||||
import { mockItems, mockBadges } from '@/data/mockItems'
|
||||
|
||||
// State Interface
|
||||
interface AppState {
|
||||
// Items & Search
|
||||
items: CuratedItem[]
|
||||
filteredItems: CuratedItem[]
|
||||
searchFilters: SearchFilters
|
||||
searchQuery: string
|
||||
|
||||
// User & Profile
|
||||
user: UserProfile | null
|
||||
isAuthenticated: boolean
|
||||
|
||||
// UI State
|
||||
loading: boolean
|
||||
error: string | null
|
||||
|
||||
// Features
|
||||
wishlist: string[]
|
||||
recentlyViewed: string[]
|
||||
}
|
||||
|
||||
// Action Types
|
||||
type AppAction =
|
||||
| { type: 'SET_ITEMS'; payload: CuratedItem[] }
|
||||
| { type: 'SET_FILTERED_ITEMS'; payload: CuratedItem[] }
|
||||
| { type: 'SET_SEARCH_QUERY'; payload: string }
|
||||
| { type: 'SET_SEARCH_FILTERS'; payload: SearchFilters }
|
||||
| { type: 'SET_USER'; payload: UserProfile | null }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'TOGGLE_WISHLIST'; payload: string }
|
||||
| { type: 'ADD_TO_RECENTLY_VIEWED'; payload: string }
|
||||
| { type: 'CLEAR_SEARCH' }
|
||||
|
||||
// Initial State
|
||||
const initialState: AppState = {
|
||||
items: [],
|
||||
filteredItems: [],
|
||||
searchFilters: {},
|
||||
searchQuery: '',
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
wishlist: [],
|
||||
recentlyViewed: []
|
||||
}
|
||||
|
||||
// Reducer
|
||||
function appReducer(state: AppState, action: AppAction): AppState {
|
||||
switch (action.type) {
|
||||
case 'SET_ITEMS':
|
||||
return {
|
||||
...state,
|
||||
items: action.payload,
|
||||
filteredItems: action.payload
|
||||
}
|
||||
|
||||
case 'SET_FILTERED_ITEMS':
|
||||
return {
|
||||
...state,
|
||||
filteredItems: action.payload
|
||||
}
|
||||
|
||||
case 'SET_SEARCH_QUERY':
|
||||
return {
|
||||
...state,
|
||||
searchQuery: action.payload
|
||||
}
|
||||
|
||||
case 'SET_SEARCH_FILTERS':
|
||||
return {
|
||||
...state,
|
||||
searchFilters: action.payload
|
||||
}
|
||||
|
||||
case 'SET_USER':
|
||||
return {
|
||||
...state,
|
||||
user: action.payload,
|
||||
isAuthenticated: action.payload !== null,
|
||||
wishlist: action.payload?.wishlist || []
|
||||
}
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
loading: action.payload
|
||||
}
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload
|
||||
}
|
||||
|
||||
case 'TOGGLE_WISHLIST':
|
||||
const itemId = action.payload
|
||||
const isWishlisted = state.wishlist.includes(itemId)
|
||||
const newWishlist = isWishlisted
|
||||
? state.wishlist.filter(id => id !== itemId)
|
||||
: [...state.wishlist, itemId]
|
||||
|
||||
return {
|
||||
...state,
|
||||
wishlist: newWishlist,
|
||||
user: state.user ? {
|
||||
...state.user,
|
||||
wishlist: newWishlist
|
||||
} : null
|
||||
}
|
||||
|
||||
case 'ADD_TO_RECENTLY_VIEWED':
|
||||
const viewedItemId = action.payload
|
||||
const newRecentlyViewed = [
|
||||
viewedItemId,
|
||||
...state.recentlyViewed.filter(id => id !== viewedItemId)
|
||||
].slice(0, 10) // Keep only last 10
|
||||
|
||||
return {
|
||||
...state,
|
||||
recentlyViewed: newRecentlyViewed
|
||||
}
|
||||
|
||||
case 'CLEAR_SEARCH':
|
||||
return {
|
||||
...state,
|
||||
searchQuery: '',
|
||||
searchFilters: {},
|
||||
filteredItems: state.items
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// Context
|
||||
const AppContext = createContext<{
|
||||
state: AppState
|
||||
dispatch: React.Dispatch<AppAction>
|
||||
// Helper functions
|
||||
searchItems: (query: string, filters?: SearchFilters) => void
|
||||
toggleWishlist: (itemId: string) => void
|
||||
addToRecentlyViewed: (itemId: string) => void
|
||||
getItemById: (id: string) => CuratedItem | undefined
|
||||
getFeaturedItems: () => CuratedItem[]
|
||||
getItemsByCategory: (category: string) => CuratedItem[]
|
||||
checkBadgeProgress: (userId: string) => Badge[]
|
||||
} | null>(null)
|
||||
|
||||
// Provider Component
|
||||
export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, dispatch] = useReducer(appReducer, initialState)
|
||||
|
||||
// Initialize data
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'SET_ITEMS', payload: mockItems })
|
||||
|
||||
// Load user from localStorage
|
||||
const savedUser = localStorage.getItem('curatedFindsUser')
|
||||
if (savedUser) {
|
||||
try {
|
||||
const user = JSON.parse(savedUser)
|
||||
dispatch({ type: 'SET_USER', payload: user })
|
||||
} catch (error) {
|
||||
console.error('Error loading user from localStorage:', error)
|
||||
}
|
||||
} else {
|
||||
// Create demo user
|
||||
const demoUser: UserProfile = {
|
||||
id: 'demo-user',
|
||||
email: 'demo@curatedfinds.com',
|
||||
username: 'CollectorDemo',
|
||||
level: 3,
|
||||
xp: 250,
|
||||
badges: [mockBadges[0], mockBadges[6]], // First Find + Daily Visitor
|
||||
collections: [],
|
||||
wishlist: ['1', '3'], // Pre-populate with some items
|
||||
streak: 5,
|
||||
preferences: {
|
||||
categories: ['photography', 'books'],
|
||||
priceRange: { min: 0, max: 500 },
|
||||
notifications: {
|
||||
email: true,
|
||||
push: false,
|
||||
priceAlerts: true,
|
||||
newFinds: true,
|
||||
weeklyDigest: true
|
||||
},
|
||||
privacy: {
|
||||
showCollections: true,
|
||||
showActivity: false
|
||||
}
|
||||
},
|
||||
joinDate: new Date('2024-11-01'),
|
||||
lastActive: new Date()
|
||||
}
|
||||
dispatch({ type: 'SET_USER', payload: demoUser })
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save user to localStorage when it changes
|
||||
useEffect(() => {
|
||||
if (state.user) {
|
||||
localStorage.setItem('curatedFindsUser', JSON.stringify(state.user))
|
||||
}
|
||||
}, [state.user])
|
||||
|
||||
// Helper Functions
|
||||
const searchItems = (query: string, filters: SearchFilters = {}) => {
|
||||
dispatch({ type: 'SET_SEARCH_QUERY', payload: query })
|
||||
dispatch({ type: 'SET_SEARCH_FILTERS', payload: filters })
|
||||
|
||||
let filtered = [...state.items]
|
||||
|
||||
// Text search
|
||||
if (query.trim()) {
|
||||
const searchTerm = query.toLowerCase()
|
||||
filtered = filtered.filter(item =>
|
||||
item.title.toLowerCase().includes(searchTerm) ||
|
||||
item.expertNotes.toLowerCase().includes(searchTerm) ||
|
||||
item.tags.some(tag => tag.toLowerCase().includes(searchTerm))
|
||||
)
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (filters.category) {
|
||||
filtered = filtered.filter(item => item.category === filters.category)
|
||||
}
|
||||
|
||||
// Price range filter
|
||||
if (filters.priceMin !== undefined) {
|
||||
filtered = filtered.filter(item => item.price >= filters.priceMin!)
|
||||
}
|
||||
if (filters.priceMax !== undefined) {
|
||||
filtered = filtered.filter(item => item.price <= filters.priceMax!)
|
||||
}
|
||||
|
||||
// Condition filter
|
||||
if (filters.condition && filters.condition.length > 0) {
|
||||
filtered = filtered.filter(item => filters.condition!.includes(item.condition))
|
||||
}
|
||||
|
||||
// Rarity filter
|
||||
if (filters.rarity && filters.rarity.length > 0) {
|
||||
filtered = filtered.filter(item => filters.rarity!.includes(item.rarity))
|
||||
}
|
||||
|
||||
// Featured filter
|
||||
if (filters.featured) {
|
||||
filtered = filtered.filter(item => item.featured)
|
||||
}
|
||||
|
||||
// Sorting
|
||||
switch (filters.sortBy) {
|
||||
case 'price-low':
|
||||
filtered.sort((a, b) => a.price - b.price)
|
||||
break
|
||||
case 'price-high':
|
||||
filtered.sort((a, b) => b.price - a.price)
|
||||
break
|
||||
case 'rarity':
|
||||
filtered.sort((a, b) => b.rarity - a.rarity)
|
||||
break
|
||||
case 'date':
|
||||
default:
|
||||
filtered.sort((a, b) => new Date(b.dateAdded).getTime() - new Date(a.dateAdded).getTime())
|
||||
break
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_FILTERED_ITEMS', payload: filtered })
|
||||
}
|
||||
|
||||
const toggleWishlist = (itemId: string) => {
|
||||
dispatch({ type: 'TOGGLE_WISHLIST', payload: itemId })
|
||||
}
|
||||
|
||||
const addToRecentlyViewed = (itemId: string) => {
|
||||
dispatch({ type: 'ADD_TO_RECENTLY_VIEWED', payload: itemId })
|
||||
}
|
||||
|
||||
const getItemById = (id: string) => {
|
||||
return state.items.find(item => item.id === id)
|
||||
}
|
||||
|
||||
const getFeaturedItems = () => {
|
||||
return state.items.filter(item => item.featured)
|
||||
}
|
||||
|
||||
const getItemsByCategory = (category: string) => {
|
||||
return state.items.filter(item => item.category === category)
|
||||
}
|
||||
|
||||
const checkBadgeProgress = (userId: string): Badge[] => {
|
||||
if (!state.user) return []
|
||||
|
||||
const earnedBadges: Badge[] = []
|
||||
const wishlistCount = state.user.wishlist.length
|
||||
|
||||
// First Find badge
|
||||
if (wishlistCount >= 1 && !state.user.badges.find(b => b.id === 'first-find')) {
|
||||
earnedBadges.push(mockBadges.find(b => b.id === 'first-find')!)
|
||||
}
|
||||
|
||||
// Category-specific badges
|
||||
const photoItems = state.user.wishlist.filter(id => {
|
||||
const item = getItemById(id)
|
||||
return item?.category === 'photography'
|
||||
}).length
|
||||
|
||||
if (photoItems >= 10 && !state.user.badges.find(b => b.id === 'camera-collector')) {
|
||||
earnedBadges.push(mockBadges.find(b => b.id === 'camera-collector')!)
|
||||
}
|
||||
|
||||
return earnedBadges
|
||||
}
|
||||
|
||||
const contextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
searchItems,
|
||||
toggleWishlist,
|
||||
addToRecentlyViewed,
|
||||
getItemById,
|
||||
getFeaturedItems,
|
||||
getItemsByCategory,
|
||||
checkBadgeProgress
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Hook to use the context
|
||||
export function useApp() {
|
||||
const context = useContext(AppContext)
|
||||
if (!context) {
|
||||
throw new Error('useApp must be used within an AppProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export default AppContext
|
||||
|
|
@ -0,0 +1,656 @@
|
|||
export interface BlogPost {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
excerpt: string
|
||||
content: string
|
||||
images: string[]
|
||||
datePublished: Date
|
||||
category: string
|
||||
tags: string[]
|
||||
featured: boolean
|
||||
previewImage?: string
|
||||
}
|
||||
|
||||
// Static image imports for bundling
|
||||
import magazine1 from '@/app/blogpostimg/magazine1.jpg'
|
||||
import magazine2 from '@/app/blogpostimg/magazine2.jpg'
|
||||
import magazine3 from '@/app/blogpostimg/magazine3.jpg'
|
||||
import magazine4 from '@/app/blogpostimg/magazine4.jpg'
|
||||
import matches1 from '@/app/blogpostimg/matches1.jpg'
|
||||
import matches2 from '@/app/blogpostimg/matches2.jpg'
|
||||
import matches3 from '@/app/blogpostimg/matches3.jpg'
|
||||
import matches4 from '@/app/blogpostimg/matches4.jpg'
|
||||
import matches5 from '@/app/blogpostimg/matches5.jpg'
|
||||
import leather_clutch1 from '@/app/blogpostimg/leather_clutch1.jpg'
|
||||
import leather_clutch2 from '@/app/blogpostimg/leather_clutch2.jpg'
|
||||
import leather_clutch3 from '@/app/blogpostimg/leather_clutch3.jpg'
|
||||
import leather_clutch4 from '@/app/blogpostimg/leather_clutch4.jpg'
|
||||
import leather_clutch5 from '@/app/blogpostimg/leather_clutch5.jpg'
|
||||
import chatgptMagazine from '@/app/blogpostimg/ChatGPT _magazine.png'
|
||||
import chatgptMatches from '@/app/blogpostimg/ChatGPT_matches.png'
|
||||
import chatgptLeatherClutch from '@/app/blogpostimg/ChatGPT_leather_clutch.png'
|
||||
import cardDeckBanner from '@/app/blogpostimg/card_deck_banner.png'
|
||||
import deckCard1 from '@/app/blogpostimg/deck_card1.png'
|
||||
import deckCard2 from '@/app/blogpostimg/deck_card2.png'
|
||||
import deckCard3 from '@/app/blogpostimg/deck_card3.png'
|
||||
import deckCard4 from '@/app/blogpostimg/deck_card4.png'
|
||||
import deckCard5 from '@/app/blogpostimg/deck_card5.png'
|
||||
import deckCard6 from '@/app/blogpostimg/deck_card6.png'
|
||||
import dollBanner from '@/app/blogpostimg/doll_banner.png'
|
||||
import doll1 from '@/app/blogpostimg/doll1.png'
|
||||
import doll2 from '@/app/blogpostimg/doll2.png'
|
||||
import doll3 from '@/app/blogpostimg/doll3.png'
|
||||
import timeMagazineBanner from '@/app/blogpostimg/time_magazine_banner.png'
|
||||
import timeMagazine1 from '@/app/blogpostimg/time_magazine1.png'
|
||||
import timeMagazine2 from '@/app/blogpostimg/time_magazine2.png'
|
||||
import timeMagazine3 from '@/app/blogpostimg/time_magazine3.png'
|
||||
|
||||
export const blogPosts: BlogPost[] = [
|
||||
{
|
||||
id: 'time-magazine-1946',
|
||||
title: 'One remarkable 1946 TIME—original Omar Bradley cover, carefully graded and ready to ship',
|
||||
slug: 'time-magazine-1946-omar-bradley',
|
||||
excerpt: 'Every so often I list something I wish I could keep. This is one of those weeks. A single, well-kept original TIME magazine from April 1, 1946.',
|
||||
content: `Every so often I list something I wish I could keep. This is one of those weeks. I'm putting up a single, well-kept original **TIME** magazine—**April 1, 1946**—the **Omar Bradley Time cover** you've probably seen in history books, but rarely in the wild. If you've been looking for a **vintage Time magazine for sale** that's more "holdable history" than dusty attic find, this one's worth a closer look.
|
||||
|
||||
I buy what I love, and I describe it like I'd want it described to me: specifics, not superlatives. I'm a collector first and a seller second, which is why you'll see both the highlights and the scuffs called out plainly.
|
||||
|
||||
## What I'm selling & why
|
||||
|
||||
This copy came from a modest estate lot I picked up near Düsseldorf—an engineer's place with the neatest shelves I've ever seen. The magazines had been stored upright in a cedar cabinet, away from sunlight and damp. When I cracked the box, the first thing I noticed (after that faint library smell) was the cover: Boris Chaliapin's painted portrait of **General Omar N. Bradley**, strong stare, clean red border. Under the title, a line asks, "How long does a war last?"—which is exactly what the features inside wrestle with.
|
||||
|
||||
I'm listing it because I already own a keeper of this exact **Time magazine 1946** issue. Duplicates go to people who'll actually read them, display them, and enjoy the odd mix of gravitas and mid-century charm.
|
||||
|
||||
## Featured item (full details)
|
||||
|
||||
**TIME Magazine — April 1, 1946 ("The Veterans' General Bradley")**
|
||||
|
||||
- **Condition:** **Good to Very Good** for age. Tight staples, square spine for a weekly, gentle rub on the red frame, light edge wear, no writing or stamps, no missing pages. Paper is supple with the expected warm tone (no brittleness).
|
||||
|
||||
- **Standout details:**
|
||||
- **Cover:** Original Boris Chaliapin portrait of Omar Bradley; color is still lively.
|
||||
- **Back cover:** Iconic Camel ad—"More Doctors Smoke Camels Than Any Other Cigarette!"—a conversation piece all by itself.
|
||||
- **Inside:** Post-war features on veterans' benefits, industry ramp-up, and domestic life. There's also a handsome B.F. Goodrich spread and, yes, **Elsie the Cow**.
|
||||
|
||||
- **Price range:** **€16–€24**, depending on whether you choose bag+board only or the archival clam-shell upgrade.
|
||||
|
||||
- **Why it's special:** This is the moment Bradley shifts from wartime commander to the public face of veteran care. If you collect military history covers, early Cold War context, or just appreciate mid-century editorial design, it checks a lot of boxes while staying genuinely readable.
|
||||
|
||||
## Behind the scenes (sourcing, care, and packaging)
|
||||
|
||||
I don't do heavy "restoration." No pressing, bleaching, or trimming. Here's my routine:
|
||||
|
||||
- **Screening:** I reject anything with mold, active foxing, or insect damage. This copy passed easily.
|
||||
- **Surface clean:** Light pass with a vinyl eraser on the back cover and margins to lift handling marks—nothing aggressive.
|
||||
- **Flattening:** A couple of days under weight between buffered sheets to relax minor waves from storage.
|
||||
- **Documentation:** I log notable ads/features and shoot close-ups of edges, corners, and staples so you can eyeball condition yourself.
|
||||
|
||||
Shipping is nerdy on purpose. The magazine goes into **mylar** with a **half-back board**, wrapped in kraft, then into a **rigid box** with corner protection. If you choose the **archival clam-shell** upgrade, you'll get a labeled, acid-free case so it can live upright on a shelf without spine stress. (And yes, I've done the "knee-height drop test." My packaging holds up.)
|
||||
|
||||
## Trust & proof
|
||||
|
||||
I've been selling vintage periodicals for years and keep things simple: accurate descriptions, fast replies, careful packing. You'll see consistent **5-star feedback** about honesty and condition photos. I ship **within 1–2 business days** from NRW. If something isn't as described, I make it right—collectors deserve straight dealing.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Is this a reprint?**
|
||||
No. This is an **original 1946** weekly magazine. You'll see the masthead date and period stock/printing consistent with **Time magazine 1946** issues. I'm happy to send extra spine and paper-tone photos on request.
|
||||
|
||||
**How do you grade condition?**
|
||||
I don't assign numeric grades. Instead I call out the important stuff: staple tightness, spine wear, edge chips, page tone, and any writing or stamps. In comic terms, this copy sits around **Good/VG**, mostly held back by light border rub and expected age toning.
|
||||
|
||||
**Can you combine shipping if I add another item?**
|
||||
Sure thing. If you grab something else from my store, I'll combine shipping and refund any overage automatically. This magazine will always ship **boxed**, even when combined.
|
||||
|
||||
**What's the return policy?**
|
||||
**30 days.** If I missed a significant flaw, I cover return shipping; if you change your mind, no problem—send it back in the same protective materials and I'll process a quick refund.
|
||||
|
||||
**How do you confirm authenticity?**
|
||||
Beyond dates and typography, I rely on period stock, staple style, and content cues (ads, features, layout grid). Provenance helps too; this one came from a tidy, smoke-free estate with a small run of mid-40s titles stored upright.
|
||||
|
||||
## Ready to make it yours?
|
||||
|
||||
If **vintage Time magazine for sale** is in your search bar, this is precisely the kind of copy you hope to click: honest condition, lots of photos, and no surprises. It reads beautifully, displays even better, and brings that immediate post-war voice into your hands—an editorial team trying to make sense of victory, grief, and the next chapter.
|
||||
|
||||
**Grab it here:** <a href="https://www.ebay.com/itm/396997013864?itmmeta=01K3RFYKHM0J79JBGG54EEB6RW&hash=item5c6eddb168:g:4dwAAeSwoqJorKN5&pfm=1" target="_blank" rel="noopener noreferrer">View on eBay</a>
|
||||
|
||||
I pack the same day when I can, the next business day when I can't. If you want the **archival clam-shell**, select the upgrade at checkout or just drop me a note—happy to add it before I seal the box. And if you're the framing type, say the word; I'll mark the Camel back cover edge that looks best in a mat so you don't lose the good bits under glass.
|
||||
|
||||
Thanks for reading—and for keeping these survivors in circulation. I like to think the editors who put this issue together would be pleased to know it's still being opened, handled, and talked about in 2025.`,
|
||||
images: [
|
||||
magazine1.src,
|
||||
magazine2.src,
|
||||
magazine3.src,
|
||||
magazine4.src
|
||||
],
|
||||
datePublished: new Date('2025-01-15'),
|
||||
category: 'Vintage Magazines',
|
||||
tags: ['TIME Magazine', 'Omar Bradley', '1946', 'Vintage', 'Military History', 'Collectibles'],
|
||||
featured: true,
|
||||
previewImage: chatgptMagazine.src
|
||||
},
|
||||
{
|
||||
id: 'vintage-las-vegas-matchbooks',
|
||||
title: 'Vintage Las Vegas Matchbooks — New eBay Drop',
|
||||
slug: 'vintage-las-vegas-matchbooks-ebay-drop',
|
||||
excerpt: 'If you love the thrill of neon and the hush of a carpeted casino hallway, you\'ll like this: a fresh batch of vintage Las Vegas matchbooks.',
|
||||
content: `If you love the thrill of neon and the hush of a carpeted casino hallway, you'll like this: I've just listed a fresh batch of vintage Las Vegas matchbooks—clear photos, fast shipping, easy returns.
|
||||
|
||||
They're small, affordable, and they tell big stories. One flips open and suddenly you're back under a pink marquee.
|
||||
|
||||
## What I'm selling & why
|
||||
|
||||
This release is a curated mix of **vintage Las Vegas matchbooks**, plus a few road-trip cousins from Texas, Colorado, and New Mexico. You'll find **casino matchbooks** from the Strip and Fremont Street alongside **hotel matchbooks** from the motor-inn era. Most are unstruck; many still have that glossy sheen that pops in a frame.
|
||||
|
||||
Why me? My family kept a shoebox of "freebies" from road stops. Dad grabbed anything with a good logo. Mom favored the typography. My job now is to rescue the best ones, research the property era, photograph them honestly, and send them to people who'll actually enjoy them. I list these because ephemera deserves a life outside a carton.
|
||||
|
||||
## Featured listings (handpicked)
|
||||
|
||||
### 1) Landmark Hotel & Casino — Space-Age Tower Cover
|
||||
|
||||
**Condition:** Unstruck; crisp fold; light edge rub only.
|
||||
**Standout detail:** Stylized tower silhouette, all Jet-Age optimism.
|
||||
**Price range:** $9–$15.
|
||||
**Why it's special:** The Landmark is long gone, which gives this cover that perfect "lost Vegas" energy. Pair it with Stardust in a two-piece frame for instant conversation.
|
||||
_Caption:_ Landmark Hotel & Casino—space-age tower art, clean and glossy.
|
||||
|
||||
### 2) Stardust Resort & Casino — Starburst Logo
|
||||
|
||||
**Condition:** Excellent; bright color; clean striker.
|
||||
**Standout detail:** That exploding starburst—Vegas without saying Vegas.
|
||||
**Price range:** $8–$12.
|
||||
**Why it's special:** If you collect **casino matchbooks**, this one's a keystone. I've had buyers tell me it's the piece that finally convinced their partner a mini-display was "actually kind of cool."
|
||||
_Caption:_ Stardust starburst—crisp colors ready for a display frame.
|
||||
|
||||
### 3) Imperial Palace — Two-Logo Mini Set
|
||||
|
||||
**Condition:** Both unstruck; mild natural toning on the ivory cover.
|
||||
**Standout detail:** Matching blue "IP" monograms—one regular, one inverted.
|
||||
**Price range:** $10–$16 (set).
|
||||
**Why it's special:** Sets display beautifully and pin a specific moment in Strip history. I like to stand these on a tiny easel next to a vintage room key.
|
||||
_Caption:_ Imperial Palace twin monograms—clean lines, great shelf presence.
|
||||
|
||||
### 4) Tropicana — "The Island of Las Vegas"
|
||||
|
||||
**Condition:** Near-mint; corners tight; colors saturated.
|
||||
**Standout detail:** Breezy palm motif and that cheeky "Island" tagline.
|
||||
**Price range:** $7–$11.
|
||||
**Why it's special:** The Trop sits at the crossroads of Rat Pack swagger and family-vacation warmth. It's the friendliest entry point for **hotel matchbooks** newcomers.
|
||||
_Caption:_ Tropicana "Island of Las Vegas"—palm-kissed getaway vibes.
|
||||
|
||||
### 5) Golden Gate Casino — Fremont Street Bridge Art
|
||||
|
||||
**Condition:** Very Good; subtle edge wrinkle noted in photos.
|
||||
**Standout detail:** Blue-and-cream bridge illustration—clean, almost nautical.
|
||||
**Price range:** $10–$14.
|
||||
**Why it's special:** The oldest hotel-casino in town carries real local lore. This cover reads more "Old Town postcard" than ad, which anchors a Fremont Street trio (add a Pioneer Club or Binion's and you're set).
|
||||
_Caption:_ Golden Gate on Fremont—bridge artwork with old-town charm.
|
||||
|
||||
### 6) Copper Manor Motel (Silver City, NM) — Road-Trip Classic
|
||||
|
||||
**Condition:** Unstruck; matte finish; honest drawer wear.
|
||||
**Standout detail:** Bold red block letters, pure roadside Americana.
|
||||
**Price range:** $5–$8.
|
||||
**Why it's special:** Not Vegas, but essential to the story: the miles between the big lights. Pair it with a Holiday Inn or La Quinta to tell the whole journey.
|
||||
_Caption:_ Copper Manor Motel—red block type that screams "road stop."
|
||||
|
||||
(You'll also see Sam's Town, Flamingo Hilton, Harrah's, Best Western, La Quinta, and some Texas pieces—perfect filler for color-coordinated frames.)
|
||||
|
||||
## Behind the scenes (how I source, clean, and ship)
|
||||
|
||||
Sourcing is part luck, part listening. Estate sales. Family binders. A shoe store owner who kept a drawer of "good logos" under the register—true story, and his taste was excellent. When a batch arrives, it goes on a clean mat. I sort by venue, then visual punch. Bold logos in one stack; understated typography in another.
|
||||
|
||||
Cleanup is gentle: a soft brush for dust; a barely damp cotton swab for glossy smudges; air-dry only. I don't polish, repaint, or press them flat—the slight tension helps a matchbook stand on a shelf. If a staple shows the first hint of rust, I neutralize and dry so the spot doesn't travel.
|
||||
|
||||
Photography happens under neutral light with a simple two-angle setup: straight-on for accuracy, tilted for sheen. Every honest quirk—tiny crease, touch of toning—shows up in the close-ups and the description. You should know exactly what's coming.
|
||||
|
||||
For packaging, singles slip into archival polypropylene sleeves and a rigid mailer with chipboard. Lots go into a small box with interleaving. Complete books ship ground and get a little padded bridge over the striker so it won't abrade in transit. **Cover-only** listings are labeled that way from the title down.
|
||||
|
||||
## Trust & proof
|
||||
|
||||
I'm a careful solo seller who ships within **one business day** and writes the kind of descriptions I'd want to read: plain, specific, no mystery. Everything's backed by a **30-day no-fuss return policy**. If I missed something meaningful, return it in the sleeve and I'll make it right.
|
||||
|
||||
Buyers who've picked up postcards and ephemera from me mention the packaging and how the item looks even better in hand. That's the standard here.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Are these originals or reprints?**
|
||||
Originals. I source from personal collections, binders, and estates. If I ever list a reproduction, it'll be labeled clearly (and priced accordingly).
|
||||
|
||||
**How do you grade condition?**
|
||||
I use Near-Mint / Excellent / Very Good / Good, with notes that pinpoint wear (edge rub, micro-crease, light toning). I always state whether the striker is clean and if matches are present.
|
||||
|
||||
**Do you combine shipping?**
|
||||
Yes. Add items to your cart and checkout; eBay will auto-combine whenever possible. If something looks off, message me before paying and I'll adjust.
|
||||
|
||||
**What about safety or smell?**
|
||||
Complete books ship via ground and are padded so the striker doesn't rub. If you're scent-sensitive, choose **cover-only** listings—paper tends to be fresher once the match heads are gone.
|
||||
|
||||
**What's your return policy?**
|
||||
Thirty days. If it's not as described—or you just changed your mind—send it back in the original sleeve and I'll refund on receipt.
|
||||
|
||||
## Ready to browse?
|
||||
|
||||
Start with one. Or start with six. Build a tiny museum: a Stardust-and-Landmark duo for "lost Vegas," an IP/Tropicana pairing for weekend-trip energy, or a Fremont Street line-up anchored by Golden Gate. Whatever story you're telling, these little books have the range.
|
||||
|
||||
👉 **Explore the full drop:** TimoKnuthVintage on eBay`,
|
||||
images: [
|
||||
matches1.src,
|
||||
matches2.src,
|
||||
matches3.src,
|
||||
matches4.src,
|
||||
matches5.src
|
||||
],
|
||||
datePublished: new Date('2025-01-10'),
|
||||
category: 'Vintage Ephemera',
|
||||
tags: ['Las Vegas', 'Matchbooks', 'Casino', 'Vintage', 'Collectibles', 'Americana'],
|
||||
featured: true,
|
||||
previewImage: chatgptMatches.src
|
||||
},
|
||||
{
|
||||
id: 'vintage-japanese-leather-clutch',
|
||||
title: 'A Vintage Japanese Leather Clutch That Tells a Story—Now on eBay',
|
||||
slug: 'vintage-japanese-leather-clutch',
|
||||
excerpt: 'A hand-painted, hand-embossed clutch that reads like three postcards in one: temple on the front, bridge inside, and Mount Fuji on the back.',
|
||||
content: `If you like objects that carry a little adventure, you'll like this one. I've just listed a **vintage Japanese leather clutch** that reads like three postcards in one: temple on the front, bridge inside, and Mount Fuji on the back.
|
||||
|
||||
It's the sort of piece you spot across a table and then keep noticing. The embossing is sharp, the colors are hand-brushed, and the surface has that warm, honest patina you can't fake.
|
||||
|
||||
## What I'm selling & why
|
||||
|
||||
Short version: a hand-painted, hand-embossed clutch made in Japan around the mid-century, often called **bunko leather**. The technique is wonderfully tactile—designs are pressed into pale leather, then each area is brushed with pigment and sealed with a protective lacquer that settles into the recesses. Over decades it develops fine **craquelure** (those hairline networks you see in old varnish) and a depth that photos never quite capture.
|
||||
|
||||
Why this one? When I first opened the flap, I realized the maker had staged a tiny journey. The outside shows an ornate shrine façade with curved roofs and lattice rails. Open it, and there's a vermilion bridge crossing a rocky river—so close you can feel the riffles under your thumb. Flip it over and Fuji appears, snow-capped and calm. Three views, one carry. It felt like meeting a small, composed person who quietly has a lot to say.
|
||||
|
||||
I gravitate to pieces that were made to last and made by hand. This clutch carries those human traces—paint that occasionally wanders over a line, shading that pools in the grout of the stonework, a slightly soft corner that tells you it was used, but carefully.
|
||||
|
||||
## Featured item
|
||||
|
||||
**Nikkō Gate, Sacred Bridge & Fuji — Hand-Painted Bunko Leather Clutch (1930s–1950s)**
|
||||
**Condition:** Good vintage. Even, fine craquelure in the lacquer; minor edge rubs; stitching intact. The metal turn-plate/snap closes with a tidy click. Interior lining shows age and remains tidy.
|
||||
**Standout detail:** A "three-scene" composition. On the flap: an elaborate shrine façade with deep green roofs and delicate fretwork—very much in the spirit of Nikkō Tōshōgū. Inside: a **vermillion bridge** with black posts, set over embossed water and stone. Back panel: **Mount Fuji**, rendered in quiet tones that balance the bolder front.
|
||||
**Approx. size:** about 25 × 13 × 3 cm—roughly the footprint of an A5 notebook; big enough for a phone, slim card case, key, and a lipstick.
|
||||
**Carry & interior:** slim hand strap across the back; fabric lining with a small vintage paper label in Japanese; inner panel fully painted with the bridge scene.
|
||||
**Palette:** moss and cedar greens, soft stone greys, warm straw, and vermilion accents.
|
||||
**Price range:** €89–€129 (check the live listing for the exact number).
|
||||
**Why it's special:** Most scenic clutches settle for a single motif. This one offers a tour and does it with restraint: deep relief lines you can feel, balanced color, and a design that wears well with modern outfits. It's the collector-grade piece you can still use.
|
||||
|
||||
You'll see detailed photos—front, back, open, and close-ups of the embossing—so you can read the surface for yourself. Nothing is hidden; the character is part of the value.
|
||||
|
||||
## Behind the scenes (sourcing, care, packaging)
|
||||
|
||||
**Sourcing.** I look for quality you can feel before you name it: crisp relief, confident brushwork (not printed), era-correct hardware, and scenes that make sense together. Temple, bridge, Fuji is a classic trio, and this example carries it with poise.
|
||||
|
||||
**Care.** Cleaning is minimal. I dust with a soft brush, wipe gently with a barely damp microfiber, and stop. Oils, alcohols, and common leather conditioners can soften or lift old pigments, so I don't use them. The goal is preservation, not refurbishment.
|
||||
|
||||
**Packaging.** The painted panels get a layer of glassine so nothing adheres or abrades. Cushioning is placed around edges and structure, not directly on the artwork. Everything is boxed firmly with corner guards. You'll receive tracking, and the unboxing is straightforward: no glitter, no drama, just safe transit for a delicate piece.
|
||||
|
||||
## Trust & proof
|
||||
|
||||
I keep my shop small on purpose so I can write precise descriptions and answer messages quickly. Buyers regularly mention accurate photos and careful packing in feedback. Orders ship from Germany within **1–2 business days** using a tracked service. There's a **30-day return window**, because buying vintage online should feel normal and safe. If you're new to this category—sometimes listed as a _bunko leather wallet_ or even a _hand painted Nikko Toshogu purse_—I'm happy to explain the telltales that separate hand-worked pieces from later prints.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: What's the exact size?**
|
||||
A: Approximately **25 × 13 × 3 cm**. It fits a modern smartphone, slim wallet, keys, and a lipstick. I photographed it next to common items for scale.
|
||||
|
||||
**Q: Summarize the condition in one sentence?**
|
||||
A: **Good vintage**—fine, even craquelure to the lacquer, light edge wear, strong color, secure hardware, clean lining for its age.
|
||||
|
||||
**Q: Can I use it as an everyday clutch?**
|
||||
A: It's sturdy for a vintage piece, but I recommend mindful use: avoid alcohol wipes and sharp keys against the painted areas; store it flat and out of direct light. Treat it like a well-made book with a painted cover.
|
||||
|
||||
**Q: Do you ship internationally?**
|
||||
A: Yes. Tracked international shipping is available. EU deliveries typically arrive in **3–7 business days**; overseas parcels usually land in **1–3 weeks** depending on customs. If you need it quickly, message me before checkout and I'll quote an express option.
|
||||
|
||||
**Q: What's your return policy?**
|
||||
A: **30 days** from delivery. Send it back in the same condition and original packaging; once it's back and checked, I issue a prompt refund.
|
||||
|
||||
**Q: How do I know it's hand-painted, not printed?**
|
||||
A: Look for tiny variations in color density, the occasional brush touch outside an embossed line, and raised contours you can feel. Those are hallmarks of the bunko-leather emboss-and-paint method. I included macro photos so you can inspect them yourself.
|
||||
|
||||
## See the listing, more photos, and the current price
|
||||
|
||||
<a href="https://www.ebay.com/itm/396996956768?itmmeta=01K3RFYKHMQVR0JKTJCGF11E6R&hash=item5c6edcd260:g:-7cAAeSwwG5orJbJ&pfm=1" target="_blank" rel="noopener noreferrer">→ View on eBay</a>
|
||||
|
||||
* * *
|
||||
|
||||
If you have a question about care, styling, or provenance, send me a note. I answer quickly and with the same detail you see here. Pieces like this don't shout; they just keep telling the story to anyone who slows down long enough to listen.`,
|
||||
images: [
|
||||
leather_clutch5.src,
|
||||
leather_clutch4.src,
|
||||
leather_clutch3.src,
|
||||
leather_clutch2.src,
|
||||
leather_clutch1.src
|
||||
],
|
||||
datePublished: new Date('2025-01-05'),
|
||||
category: 'Vintage Fashion',
|
||||
tags: ['Japanese', 'Bunko Leather', 'Vintage', 'Hand-painted', 'Clutch', 'Mount Fuji', 'Collectibles'],
|
||||
featured: false,
|
||||
previewImage: chatgptLeatherClutch.src
|
||||
},
|
||||
{
|
||||
id: 'vintage-bicycle-808-rider-back-blue-sealed',
|
||||
title: 'Vintage Bicycle 808 Rider Back deck (blue), sealed',
|
||||
slug: 'vintage-bicycle-808-rider-back-blue-sealed',
|
||||
excerpt: 'A sealed Bicycle Rider Back No. 808 poker deck from the tax-stamp era. Blue back, air-cushion finish, with the original revenue stamp and red tear-strip visible.',
|
||||
content: `Every so often I list something I wish I could keep. This is one of those weeks. I'm putting up a sealed **Bicycle Rider Back No. 808** deck from the tax-stamp era—blue back, air-cushion finish, with the original stamp and red tear-strip visible.
|
||||
|
||||
It's the sort of piece that makes collectors pause. The intact revenue stamp, the period-correct typography, the way the cellophane has aged—this is a time capsule from the mid-20th century that's still factory-sealed.
|
||||
|
||||
## What it is
|
||||
|
||||
This post takes a closer look at a Bicycle Rider Back No. 808 poker-size deck made in the United States by The U.S. Playing Card Co. It's a factory-sealed, tax-stamp era example. In the photos, the U.S. revenue stamp bridges the top flap and the thin red ribbon sits beneath the outer wrap—two small packaging cues that place it pre-1965. The listing describes the deck as "brand-new old stock" in its original packaging with the familiar Air-Cushion Finish.
|
||||
|
||||
The back color is blue, and the box carries period side text—"AIR-CUSHION FINISH," "808," "THE U. S. PLAYING CARD CO.," "MADE IN U.S.A."—with no barcode, which matches the timeframe. The overall impression is a tidy, mid-century Bicycle that has stayed intact: warm ivory tuck paper, clean printing, and a sealed flap. It's the kind of small, practical collectible that sits easily on a shelf yet still speaks to everyday use—standard poker size, the classic Rider Back artwork, and typography most card players recognize at a glance.
|
||||
|
||||
If you care about subtle differences between runs, this one leans into that quiet mid-century look: softer paper tone, modest gloss on the wrap, and consistent halftone detail on the logos. If you just want a reliable piece of design history, it's simply a sealed Bicycle 808 that hasn't been fussed with—nothing flashy, just well-kept.
|
||||
|
||||
## Small details that matter
|
||||
|
||||
**Front tuck artwork:** Tall BICYCLE logotype above the red Ace of Spades emblem. The ivory-toned paperboard shows an even patina, with crisp edges around the red spade.
|
||||
|
||||
**Back design panel:** The blue Rider Back artwork (mirrored angels, scrollwork, circular hub) reads clearly through the wrap; lines look sharp rather than mushy.
|
||||
|
||||
**Tax stamp placement:** A U.S. revenue stamp crosses the top flap. Edges and micro-print are visible beneath the cellophane; the stamp appears unbroken.
|
||||
|
||||
**Ribbon/tear-strip:** The thin red band is visible within the wrap—typical on mid-century Bicycle decks—and gives a faint linear reflection under light.
|
||||
|
||||
**Side typography:** "AIR-CUSHION FINISH," "808," "THE U. S. PLAYING CARD CO.," "MADE IN U.S.A." set in period fonts with neat baselines; no barcode present.
|
||||
|
||||
**Wrap condition:** The cellophane shows fine crinkles and micro-scuffs consistent with storage. Seams are tidy, with a soft gloss rather than high shine.
|
||||
|
||||
**Corners & edges:** A small compression at the top-left corner of the tuck; other corners look square with even folds.
|
||||
|
||||
**Color palette:** Warm ivory tuck stock, midnight blue back art, and red accents (tear-strip, tax stamp, spade graphic) produce a simple tri-color look.
|
||||
|
||||
**Scale:** Standard poker deck proportions. Exact measurements not specified; fits common card clips and cases.
|
||||
|
||||
**Era cues:** Lack of UPC, stamp across flap, red ribbon under wrap—all typical pre-1965 signals.
|
||||
|
||||
**Print quality:** Halftones and outlines on logos appear intact; no obvious ink bleed visible in the photos.
|
||||
|
||||
**Paper texture:** Subtle paper tooth shows through in raking light; not glossy tuck stock.
|
||||
|
||||
**Flap/top panel:** "808" printed on the top panel is clear; edges around the fold look straight without tearing.
|
||||
|
||||
**Made in U.S.A. panel:** The "MADE IN U.S.A." side is readable; slight toning at the edges suggests normal age rather than heavy wear.
|
||||
|
||||
**Lighting reflections:** Highlights along the right edge reveal the cellophane's soft sheen; no blown-out glare.
|
||||
|
||||
## Everyday use
|
||||
|
||||
**Shelf or desk display:** A compact visual anchor for a bookcase, studio shelf, or office credenza. The blue back and red accents add a quiet bit of color without shouting.
|
||||
|
||||
**Gift for players and magicians:** A low-key present for someone who appreciates classic decks. The sealed state lets the recipient decide whether to keep it closed or open it later.
|
||||
|
||||
**Photo/film prop:** Useful for Americana, magic, or mid-century sets where real paper texture, tax stamps, and era-accurate packaging matter more than replicas.
|
||||
|
||||
**Reference for designers:** If you collect packaging or study typography, this tuck shows mid-century print tone, line thickness, and label placement in a compact format.
|
||||
|
||||
**Conversation piece:** Lives easily on a side table; guests recognize it and usually have a story about game nights, shuffles, or a card trick.
|
||||
|
||||
## Condition & notes
|
||||
|
||||
**Seller statement:** "Brand-new old stock — sealed, untouched deck in original packaging."
|
||||
|
||||
**Dating:** Pre-1965 based on tax stamp and ribbon seal; exact production year not verified.
|
||||
|
||||
**Observed wear (from photos):**
|
||||
|
||||
Mild age toning to the ivory tuck, even across panels.
|
||||
|
||||
Micro-scuffs and tiny wrinkles in the outer wrap from storage.
|
||||
|
||||
Small corner ding at the top-left; paper compression is visible but localized.
|
||||
|
||||
Printing remains crisp; panel edges look straight with no visible tears.
|
||||
|
||||
**Completeness:** Inner contents remain unopened; jokers/ad cards not specified.
|
||||
|
||||
**Grading:** No third-party grading or authentication mentioned.
|
||||
|
||||
**Odor/storage:** Not verified; listing doesn't note smoke exposure or storage environment.
|
||||
|
||||
**Provenance:** Not specified beyond photos and general era cues.
|
||||
|
||||
If you track production variants, a photo of the bottom flap (not shown here) can sometimes narrow down the print window further. As it stands, the visible stamp, ribbon, and side text are enough to keep it comfortably in the late 1950s–early 1960s range without over-claiming.
|
||||
|
||||
## Care & compatibility
|
||||
|
||||
**Storage basics:** Keep upright in a cool, dry room (roughly 30–50% RH if you track humidity). Avoid attics, basements, or windowsills—heat and UV can embrittle cellophane and fade inks.
|
||||
|
||||
**Light exposure:** Display in indirect light. If you like a brighter shelf, consider a spot with filtered daylight. Prolonged direct sun can yellow paper and loosen old adhesives.
|
||||
|
||||
**Handling:** Lift from the bottom panel and support the sides to avoid stressing the top-left corner. Finger oils can spot the tuck—clean, dry hands help.
|
||||
|
||||
**Protection:** An acid-free sleeve or a small acrylic case keeps dust down while letting the graphics show. Avoid tape or adhesive tabs on the tuck or stamp.
|
||||
|
||||
**If opening:** Use the red tear-strip along the seam. Avoid peeling across the stamp—slice the wrap rather than lifting the tax stamp. Open over a clean surface in case any tiny paper fibers shed.
|
||||
|
||||
**After opening:** If you plan to use the deck, consider a card clip or a simple tuck case to limit warping. Finish and exact stock inside are presumed standard for Bicycle of the era but not verified without opening.
|
||||
|
||||
**Compatibility:** Standard poker size. Fits most modern card shufflers, holders, and shelves. Sleeve sizes vary by maker; check dimensions if you're buying a snug acrylic case.
|
||||
|
||||
**Cleaning the exterior:** Dust with a soft, dry cloth. Don't use liquids on cellophane; moisture can creep under edges and cloud the wrap.
|
||||
|
||||
## If you're curious
|
||||
|
||||
If you'd like more photos, exact measurements, or shipping details, check the seller's listing: <a href="https://www.ebay.com/itm/396997017344?itmmeta=01K3RFYKHMETXQNJ5A2YER0A4G&hash=item5c6eddbf00:g:WvUAAeSwesRorNwH&pfm=1" target="_blank" rel="noopener noreferrer">View on eBay</a>
|
||||
|
||||
`,
|
||||
images: [
|
||||
cardDeckBanner.src,
|
||||
deckCard1.src,
|
||||
deckCard2.src,
|
||||
deckCard3.src,
|
||||
deckCard4.src,
|
||||
deckCard5.src,
|
||||
deckCard6.src
|
||||
],
|
||||
datePublished: new Date('2025-01-20'),
|
||||
category: 'Vintage Collectibles',
|
||||
tags: ['Bicycle', 'playing-cards', 'Rider-Back-808', 'vintage', 'blue', 'sealed', 'tax-stamp', 'collectibles'],
|
||||
featured: true,
|
||||
previewImage: cardDeckBanner.src
|
||||
},
|
||||
{
|
||||
id: 'vintage-forest-troll-4-5in-fuchsia-hair',
|
||||
title: 'Vintage Forest Troll, 4.5" with fuchsia hair',
|
||||
slug: 'vintage-forest-troll-4-5in-fuchsia-hair',
|
||||
excerpt: 'Palm-size vinyl troll with bright fuchsia hair and pink eyes. Stamped "FOREST TROLL ©" and "CHINA," approximately 4.5 inches; offered without clothing.',
|
||||
content: `Every so often I list something that brings back memories. This is one of those weeks. I'm putting up a **Vintage Forest Troll** doll with bright fuchsia hair—about 4.5 inches tall, stamped "FOREST TROLL ©" and "CHINA" on the soles.
|
||||
|
||||
It's the sort of piece that makes collectors smile. The vivid hair, the familiar grin, the way it stands perfectly on a desk or shelf—this is a classic 90s toy that's still full of personality.
|
||||
|
||||
## What it is
|
||||
|
||||
This is a compact vintage Forest Troll figure with the familiar open-armed pose, a cheerful grin, and tall, up-swept fuchsia hair. The seller measures the body height at ~4.5 in / 11.5 cm (hair adds visible height but isn't part of the given measurement). The soles are stamped "FOREST TROLL ©" and "CHINA," with a small "3" that likely denotes mold or size. It's a straightforward, clothes-free example—easy to display on a shelf or desk, or to use as a base for simple customization.
|
||||
|
||||
## Small details that matter
|
||||
|
||||
**Hair color & texture:** saturated fuchsia/magenta with natural flyaways; the fibers lift and fan outward without heavy styling.
|
||||
|
||||
**Eyes:** pink, glossy inserts that catch small specular highlights; both eyes appear evenly colored in the photos.
|
||||
|
||||
**Face sculpt:** classic rounded cheeks, small nose, soft smile; no painted lips (the look comes from the vinyl tone).
|
||||
|
||||
**Vinyl finish:** tan tone with a light satin sheen; reflects softly under studio lighting; no tacky/shiny patches visible in the photos.
|
||||
|
||||
**Body sculpt:** crisp details at the belly button, toes, and ears; proportion typical for 4–5" trolls.
|
||||
|
||||
**Foot stamps:** FOREST TROLL © and CHINA are clear; the tiny "3" sits near the wording and reads sharply.
|
||||
|
||||
**Standing stance:** flat feet enable freestanding display on flat surfaces; a tiny bit of putty can improve stability if needed.
|
||||
|
||||
**Belly jewel:** not present (this is the plain-torso style, not the jeweled Treasure Troll type).
|
||||
|
||||
**Accessories:** none included; any comb or hang-tag seen in styled images is for presentation only.
|
||||
|
||||
**Scale cues:** roughly palm-size; fits beside a mug, pencil cup, or small succulent without crowding a surface.
|
||||
|
||||
**Color balance in photos:** neutral white background helps the hair read true; the fuchsia acts as the main accent color.
|
||||
|
||||
**Overall vibe:** bright hair + clean sculpt = a simple retro accent with a small footprint.
|
||||
|
||||
## Everyday use
|
||||
|
||||
**Desk companion:** adds a friendly pop of color next to a monitor or notebook. It occupies very little space and doesn't block screens or papers.
|
||||
|
||||
**Bookshelf accent:** sits well among paperbacks, small plants, or postcards; the hair adds vertical interest without needing a stand.
|
||||
|
||||
**Light craft base:** a good starting point for a mini outfit, ribbon bow, tiny hat, or a gentle hair refresh. No advanced tools required.
|
||||
|
||||
**Nostalgia gift:** a low-key 90s throwback for someone who remembers troll dolls—simple, easy to wrap, and fun to place on a shelf.
|
||||
|
||||
**Photo prop:** brings a small burst of color to casual product shots, flat lays, or birthday table setups without stealing the scene.
|
||||
|
||||
## Condition & notes
|
||||
|
||||
**Ownership:** pre-owned.
|
||||
|
||||
**Hair:** bright, saturated color with the typical frizz these dolls often show; strands appear intact at the hairline.
|
||||
|
||||
**Vinyl:** minimal handling wear visible in photos; no cracks are apparent. Fine micro-scuffs consistent with age are possible.
|
||||
|
||||
**Marks & ID:** foot stamps are intact and legible—FOREST TROLL © / CHINA plus a small "3."
|
||||
|
||||
**Brand mention:** the seller lists IMM in item specifics; this brand is not verified on the doll itself (stamps show Forest Troll/China only).
|
||||
|
||||
**Year:** not specified. Styling suggests the 1990s gift-shop era, but that timing is not verified.
|
||||
|
||||
**Packaging & extras:** none included.
|
||||
|
||||
**Measurements:** body height of ~4.5" provided by the seller; hair-to-tip height not specified and would be taller.
|
||||
|
||||
**Photos vs. reality:** colors can vary by screen; white background shots aim to keep color honest, but exact tone may differ slightly in person.
|
||||
|
||||
If any detail is critical to you (exact hair height, weight, or color match), consider asking the seller for confirmation; measurements and hues are not independently verified here.
|
||||
|
||||
## Care & compatibility
|
||||
|
||||
**Quick refresh for hair:**
|
||||
|
||||
Comb gently with a wide-tooth comb from ends upward.
|
||||
|
||||
If needed, wash in cool water with a drop of mild shampoo; rinse well.
|
||||
|
||||
Optionally, a teaspoon of diluted fabric softener in the final rinse can calm frizz; do a brief water rinse afterward.
|
||||
|
||||
Air-dry; shape the hair as it dries. Avoid heat tools.
|
||||
|
||||
**Vinyl cleaning:** wipe with a soft, damp cloth. For marks, use a tiny amount of diluted dish soap, then dry immediately. A very light touch with a melamine sponge can reduce scuffs—test a hidden spot first.
|
||||
|
||||
**Display tips:** keep out of direct sun to reduce color fade. On slick shelves, a dot of museum putty under one foot improves stability.
|
||||
|
||||
**Storage:** stand the doll upright in a small box; support the hair loosely to prevent flattening. Avoid high humidity, which can encourage frizz or dust cling.
|
||||
|
||||
**Safety:** contains small parts and loose fibers; use care around young children and pets. No batteries or electronics to maintain.
|
||||
|
||||
## If you're curious
|
||||
|
||||
If you'd like more photos, exact measurements, or shipping details, check the seller's listing: <a href="https://www.ebay.com/itm/396999861739?itmmeta=01K3RFYKHMRP2XWV0E8R1GGH0S&hash=item5c6f0925eb:g:VZ8AAeSwgvJoryzH&pfm=1" target="_blank" rel="noopener noreferrer">View on eBay</a>
|
||||
|
||||
`,
|
||||
images: [
|
||||
dollBanner.src,
|
||||
doll1.src,
|
||||
doll2.src,
|
||||
doll3.src
|
||||
],
|
||||
datePublished: new Date('2025-01-25'),
|
||||
category: 'Vintage Toys',
|
||||
tags: ['forest troll', 'troll doll', 'vintage toy', 'vinyl', 'fuchsia hair', 'pink eyes', 'made in china', '1990s style'],
|
||||
featured: false,
|
||||
previewImage: dollBanner.src
|
||||
},
|
||||
{
|
||||
id: 'time-magazine-1955-01-10-bull-market-issue',
|
||||
title: 'TIME Magazine, Jan 10, 1955 — The Bull Market Issue',
|
||||
slug: 'time-magazine-1955-01-10-bull-market-issue',
|
||||
excerpt: 'A 1955 TIME issue with Boris Chaliapin\'s bull market cover. Honest wear, clean interiors per seller, and the kind of mid-century ads people like to leaf through.',
|
||||
content: `Every so often I list something that captures a moment in history. This is one of those weeks. I'm putting up an original **TIME Magazine** dated January 10, 1955 (Vol. LXV, No. 2) with the cover theme "The Bull Market – Business Review & Forecast."
|
||||
|
||||
It's the sort of piece that makes collectors pause. The classic red border, the yellow diagonal band, the Boris Chaliapin cover art—this is a snapshot of post-war finance and magazine design that's still relevant today.
|
||||
|
||||
## What it is
|
||||
|
||||
An original TIME Magazine dated January 10, 1955 (Vol. LXV, No. 2). The cover feature is "The Bull Market – Business Review & Forecast," illustrated by Boris Chaliapin. The seller describes it as Very Good to Good for its age: softcover/wraps, English language, all pages present, no writing or tears reported, and professionally handled. Packaging is noted as archival-safe sleeve with protective cardboard, which makes storage and gifting straightforward. For anyone who enjoys mid-century reporting, period graphics, and the look of vintage paper, this issue is a quiet, practical pick.
|
||||
|
||||
## Small details that matter
|
||||
|
||||
**Classic red TIME border** with a yellow diagonal sash for the cover theme.
|
||||
|
||||
**TWENTY CENTS cover price** at the top—typical of the era's layout.
|
||||
|
||||
**Chaliapin's art shows** a bull wrapped in ticker tape, with 1950s cars, skyline hints, and industrial motifs.
|
||||
|
||||
**Original subscriber mailing label** at the lower-left of the front cover (visible in photos).
|
||||
|
||||
**Back cover:** full-color Chesterfield ad featuring Rory Calhoun & Lita Baron beside a car.
|
||||
|
||||
**Interior masthead confirms** January 10, 1955 — Vol. LXV, No. 2.
|
||||
|
||||
**Light spine stress lines;** modest corner/edge rub consistent with browsing.
|
||||
|
||||
**Small surface scuffs** on the cover; tones and inks remain strong for age.
|
||||
|
||||
**Coated magazine stock** with normal mid-century halftone dot pattern visible on close look.
|
||||
|
||||
**Age toning expected** on interior pages; paper color varies slightly by section.
|
||||
|
||||
**Binding:** softcover, wraps; exact dimensions not specified in the listing.
|
||||
|
||||
**Seller highlights** clean interiors, no writing, no tears observed.
|
||||
|
||||
**Shipped sleeved and backed,** which helps prevent additional edge wear in transit.
|
||||
|
||||
## Everyday use
|
||||
|
||||
**Desk or bookshelf display** in a study or office—pairs easily with wood and brass accents.
|
||||
|
||||
**Frame the cover** (simple black or walnut frame) for low-key mid-century wall decor in a trading nook, workspace, or hallway.
|
||||
|
||||
**Photo/film prop** for finance, newsroom, or 1950s scenes—recognizable red border reads well on camera.
|
||||
|
||||
**Coffee-table flip-through** for guests who like period ads and historical snapshots without committing to a full book.
|
||||
|
||||
## Condition & notes
|
||||
|
||||
Listed as Very Good to Good overall. Expect light edge/corner wear, minor spine creasing, and small surface scuffs on the cover. Colors look lively for the age. The seller notes all pages present, no writing or tears observed, and clean interiors with typical age toning. The front mailing label remains, which some collectors prefer to keep for provenance. Page-by-page verification beyond the photos is not verified. Odor, annotations, or clipped coupons are not specified. If you need a specific article or ad checked, it's reasonable to ask the seller for a quick look before purchase.
|
||||
|
||||
## Care & compatibility
|
||||
|
||||
**Handle with clean, dry hands;** support the spine when opening.
|
||||
|
||||
**Store flat** in the acid-free sleeve with backing board (included per listing).
|
||||
|
||||
**Keep away from** direct sunlight, humidity, and temperature swings; a closet shelf or drawer works well.
|
||||
|
||||
**If framing,** use UV-filter glazing and an acid-free mat; avoid tight clamping that presses the spine.
|
||||
|
||||
**For dust,** use a soft brush only—no liquids or solvent cleaners on coated paper.
|
||||
|
||||
**Do not attempt** label removal; adhesives on mid-century coated stock can pull ink or leave sheen changes.
|
||||
|
||||
**Allow light airing** in a dry room if there's typical vintage paper scent; avoid attic/basement storage.
|
||||
|
||||
## If you're curious
|
||||
|
||||
If you'd like more photos or specifics, check the seller's listing: <a href="https://www.ebay.com/itm/396997015021?itmmeta=01K3RFYKHM6RJ178JG8FB6BQ45&hash=item5c6eddb5ed:g:tYYAAeSwSYxoreBI&pfm=1" target="_blank" rel="noopener noreferrer">View on eBay</a>
|
||||
|
||||
`,
|
||||
images: [
|
||||
timeMagazineBanner.src,
|
||||
timeMagazine1.src,
|
||||
timeMagazine2.src,
|
||||
timeMagazine3.src
|
||||
],
|
||||
datePublished: new Date('2025-01-30'),
|
||||
category: 'Vintage Magazines',
|
||||
tags: ['Time', 'magazine', 'paper', 'mid-century', '1950s', 'business-finance', 'red-yellow', 'vintage-ads'],
|
||||
featured: false,
|
||||
previewImage: timeMagazineBanner.src
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
import { CuratedItem } from '@/types'
|
||||
|
||||
export const mockItems: CuratedItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Canon AE-1 Program 35mm SLR Camera with 50mm f/1.8 Lens',
|
||||
price: 89,
|
||||
originalPrice: 120,
|
||||
category: 'photography',
|
||||
ebayUrl: 'https://ebay.com/item/1',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1606983340126-99ab4feaa64a?w=800',
|
||||
'https://images.unsplash.com/photo-1502920917128-1aa500764cbd?w=800'
|
||||
],
|
||||
condition: 'excellent',
|
||||
rarity: 4,
|
||||
expertNotes: 'This Canon AE-1 Program is in exceptional condition with minimal wear. The light meter is accurate, and the shutter speeds are consistent. Perfect for film photography enthusiasts looking for a reliable workhorse camera.',
|
||||
dateAdded: new Date('2024-12-15'),
|
||||
featured: true,
|
||||
tags: ['35mm', 'SLR', 'Canon', 'Film Photography', 'Vintage'],
|
||||
seller: {
|
||||
name: 'VintageGearPro',
|
||||
rating: 4.9,
|
||||
feedback: 2847
|
||||
},
|
||||
shipping: {
|
||||
cost: 12,
|
||||
location: 'California, USA',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Serial number verified, original Canon parts confirmed'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Pink Floyd - The Dark Side of the Moon Original 1973 UK Pressing',
|
||||
price: 45,
|
||||
originalPrice: 65,
|
||||
category: 'music',
|
||||
ebayUrl: 'https://ebay.com/item/2',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=800',
|
||||
'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=800'
|
||||
],
|
||||
condition: 'very-good',
|
||||
rarity: 5,
|
||||
expertNotes: 'Original UK pressing with solid triangle label. Matrix numbers confirm authenticity. Minor surface marks but plays beautifully. A must-have for any serious vinyl collector.',
|
||||
dateAdded: new Date('2024-12-14'),
|
||||
featured: false,
|
||||
tags: ['Pink Floyd', 'Progressive Rock', '1970s', 'UK Pressing', 'Rare'],
|
||||
seller: {
|
||||
name: 'RecordCollectorUK',
|
||||
rating: 4.8,
|
||||
feedback: 1523
|
||||
},
|
||||
shipping: {
|
||||
cost: 8,
|
||||
location: 'London, UK',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Matrix numbers match original pressing, label verified'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'First Edition "To Kill a Mockingbird" by Harper Lee (1960)',
|
||||
price: 185,
|
||||
originalPrice: 250,
|
||||
category: 'books',
|
||||
ebayUrl: 'https://ebay.com/item/3',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=800',
|
||||
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800'
|
||||
],
|
||||
condition: 'good',
|
||||
rarity: 5,
|
||||
expertNotes: 'True first edition with all correct points: "Mockingbird" on spine, correct publisher, and first state dust jacket. Some edge wear but structurally sound. A literary treasure.',
|
||||
dateAdded: new Date('2024-12-13'),
|
||||
featured: true,
|
||||
tags: ['First Edition', 'Harper Lee', 'American Literature', 'Classic', 'Investment'],
|
||||
seller: {
|
||||
name: 'RareBookHunter',
|
||||
rating: 4.9,
|
||||
feedback: 892
|
||||
},
|
||||
shipping: {
|
||||
cost: 15,
|
||||
location: 'New York, USA',
|
||||
international: false
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'All first edition points verified by book expert'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Eames Molded Plywood Chair (DCW) Herman Miller 1950s',
|
||||
price: 320,
|
||||
originalPrice: 450,
|
||||
category: 'design',
|
||||
ebayUrl: 'https://ebay.com/item/4',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=800',
|
||||
'https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800'
|
||||
],
|
||||
condition: 'very-good',
|
||||
rarity: 4,
|
||||
expertNotes: 'Authentic Herman Miller DCW chair from the 1950s. Original Evans production with correct shock mounts. Some patina on the metal base adds character. A design icon.',
|
||||
dateAdded: new Date('2024-12-12'),
|
||||
featured: false,
|
||||
tags: ['Eames', 'Herman Miller', 'Mid-Century', 'DCW', 'Design Icon'],
|
||||
seller: {
|
||||
name: 'ModernClassics',
|
||||
rating: 4.7,
|
||||
feedback: 634
|
||||
},
|
||||
shipping: {
|
||||
cost: 45,
|
||||
location: 'Chicago, USA',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Herman Miller label present, construction details verified'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Leica IIIf 35mm Rangefinder Camera with Summicron 50mm f/2',
|
||||
price: 450,
|
||||
originalPrice: 600,
|
||||
category: 'photography',
|
||||
ebayUrl: 'https://ebay.com/item/5',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1606983340126-99ab4feaa64a?w=800'
|
||||
],
|
||||
condition: 'excellent',
|
||||
rarity: 5,
|
||||
expertNotes: 'Exceptional Leica IIIf in working condition. Rangefinder is bright and accurate, shutter speeds are consistent. The Summicron lens is sharp with minimal haze. A photographer\'s dream.',
|
||||
dateAdded: new Date('2024-12-11'),
|
||||
featured: true,
|
||||
tags: ['Leica', 'Rangefinder', 'Summicron', 'German Engineering', 'Professional'],
|
||||
seller: {
|
||||
name: 'LeicaSpecialist',
|
||||
rating: 5.0,
|
||||
feedback: 456
|
||||
},
|
||||
shipping: {
|
||||
cost: 25,
|
||||
location: 'Germany',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Serial numbers verified, CLA performed by certified technician'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'The Beatles - Please Please Me Mono UK First Pressing (1963)',
|
||||
price: 125,
|
||||
originalPrice: 180,
|
||||
category: 'music',
|
||||
ebayUrl: 'https://ebay.com/item/6',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=800'
|
||||
],
|
||||
condition: 'good',
|
||||
rarity: 5,
|
||||
expertNotes: 'Original UK mono pressing with gold and black Parlophone label. Matrix numbers confirm first pressing. Some surface wear but plays well. Historic significance cannot be overstated.',
|
||||
dateAdded: new Date('2024-12-10'),
|
||||
featured: false,
|
||||
tags: ['The Beatles', 'Mono', 'First Pressing', 'Parlophone', 'Historic'],
|
||||
seller: {
|
||||
name: 'BeatlesVinyl',
|
||||
rating: 4.9,
|
||||
feedback: 2156
|
||||
},
|
||||
shipping: {
|
||||
cost: 12,
|
||||
location: 'Liverpool, UK',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Matrix numbers and label details verified as first pressing'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'Signed First Edition "On the Road" by Jack Kerouac (1957)',
|
||||
price: 850,
|
||||
originalPrice: 1200,
|
||||
category: 'books',
|
||||
ebayUrl: 'https://ebay.com/item/7',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=800'
|
||||
],
|
||||
condition: 'very-good',
|
||||
rarity: 5,
|
||||
expertNotes: 'Signed first edition of this Beat Generation masterpiece. Signature authenticated by PSA. Dust jacket present with minor edge wear. A literary holy grail for collectors.',
|
||||
dateAdded: new Date('2024-12-09'),
|
||||
featured: true,
|
||||
tags: ['Signed', 'First Edition', 'Jack Kerouac', 'Beat Generation', 'PSA Authenticated'],
|
||||
seller: {
|
||||
name: 'LiteraryTreasures',
|
||||
rating: 4.8,
|
||||
feedback: 234
|
||||
},
|
||||
shipping: {
|
||||
cost: 20,
|
||||
location: 'San Francisco, USA',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Signature authenticated by PSA, first edition points verified'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'George Nelson Ball Clock Howard Miller 1950s Original',
|
||||
price: 280,
|
||||
originalPrice: 380,
|
||||
category: 'design',
|
||||
ebayUrl: 'https://ebay.com/item/8',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=800'
|
||||
],
|
||||
condition: 'excellent',
|
||||
rarity: 4,
|
||||
expertNotes: 'Original George Nelson Ball Clock by Howard Miller. All original balls and mechanism. Keeps perfect time. A mid-century modern icon that defines an era.',
|
||||
dateAdded: new Date('2024-12-08'),
|
||||
featured: false,
|
||||
tags: ['George Nelson', 'Howard Miller', 'Ball Clock', 'Mid-Century', 'Atomic Age'],
|
||||
seller: {
|
||||
name: 'AtomicDesign',
|
||||
rating: 4.6,
|
||||
feedback: 789
|
||||
},
|
||||
shipping: {
|
||||
cost: 18,
|
||||
location: 'Los Angeles, USA',
|
||||
international: true
|
||||
},
|
||||
authenticity: {
|
||||
verified: true,
|
||||
notes: 'Howard Miller label present, original mechanism confirmed'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export const mockBadges = [
|
||||
{
|
||||
id: 'first-find',
|
||||
name: 'First Find',
|
||||
description: 'Added your first item to wishlist',
|
||||
icon: '🎯',
|
||||
rarity: 'common' as const
|
||||
},
|
||||
{
|
||||
id: 'camera-collector',
|
||||
name: 'Camera Connoisseur',
|
||||
description: 'Wishlisted 10 photography items',
|
||||
icon: '📸',
|
||||
rarity: 'rare' as const
|
||||
},
|
||||
{
|
||||
id: 'vinyl-virtuoso',
|
||||
name: 'Vinyl Virtuoso',
|
||||
description: 'Wishlisted 15 music items',
|
||||
icon: '🎵',
|
||||
rarity: 'rare' as const
|
||||
},
|
||||
{
|
||||
id: 'book-bibliophile',
|
||||
name: 'Book Bibliophile',
|
||||
description: 'Wishlisted 10 book items',
|
||||
icon: '📚',
|
||||
rarity: 'rare' as const
|
||||
},
|
||||
{
|
||||
id: 'design-devotee',
|
||||
name: 'Design Devotee',
|
||||
description: 'Wishlisted 10 design items',
|
||||
icon: '🎨',
|
||||
rarity: 'rare' as const
|
||||
},
|
||||
{
|
||||
id: 'treasure-hunter',
|
||||
name: 'Treasure Hunter',
|
||||
description: 'Found 50 items across all categories',
|
||||
icon: '💎',
|
||||
rarity: 'legendary' as const
|
||||
},
|
||||
{
|
||||
id: 'daily-visitor',
|
||||
name: 'Daily Visitor',
|
||||
description: 'Visited the site for 7 consecutive days',
|
||||
icon: '🔥',
|
||||
rarity: 'common' as const
|
||||
},
|
||||
{
|
||||
id: 'expert-eye',
|
||||
name: 'Expert Eye',
|
||||
description: 'Wishlisted 5 items with rarity 5',
|
||||
icon: '👁️',
|
||||
rarity: 'legendary' as const
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
// Core Item Types
|
||||
export interface CuratedItem {
|
||||
id: string
|
||||
title: string
|
||||
price: number
|
||||
originalPrice?: number
|
||||
category: 'photography' | 'music' | 'books' | 'design'
|
||||
ebayUrl: string
|
||||
images: string[]
|
||||
condition: 'new' | 'like-new' | 'excellent' | 'very-good' | 'good' | 'acceptable'
|
||||
rarity: 1 | 2 | 3 | 4 | 5
|
||||
expertNotes: string
|
||||
dateAdded: Date
|
||||
featured: boolean
|
||||
tags: string[]
|
||||
seller: {
|
||||
name: string
|
||||
rating: number
|
||||
feedback: number
|
||||
}
|
||||
shipping: {
|
||||
cost: number
|
||||
location: string
|
||||
international: boolean
|
||||
}
|
||||
authenticity: {
|
||||
verified: boolean
|
||||
notes?: string
|
||||
}
|
||||
}
|
||||
|
||||
// User & Gamification Types
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
avatar?: string
|
||||
level: number // Collector Level (1-50)
|
||||
xp: number // Experience Points
|
||||
badges: Badge[] // Achievement Badges
|
||||
collections: Collection[]
|
||||
wishlist: string[] // Item IDs
|
||||
streak: number // Daily visit streak
|
||||
preferences: UserPreferences
|
||||
joinDate: Date
|
||||
lastActive: Date
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: string
|
||||
name: string // "Vinyl Virtuoso", "Camera Connoisseur"
|
||||
description: string
|
||||
icon: string
|
||||
rarity: 'common' | 'rare' | 'legendary'
|
||||
unlockedAt?: Date
|
||||
progress?: {
|
||||
current: number
|
||||
required: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Collection {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
items: string[] // Item IDs
|
||||
isPublic: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
categories: string[]
|
||||
priceRange: {
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
notifications: {
|
||||
email: boolean
|
||||
push: boolean
|
||||
priceAlerts: boolean
|
||||
newFinds: boolean
|
||||
weeklyDigest: boolean
|
||||
}
|
||||
privacy: {
|
||||
showCollections: boolean
|
||||
showActivity: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Search & Filter Types
|
||||
export interface SearchFilters {
|
||||
query?: string
|
||||
category?: string
|
||||
priceMin?: number
|
||||
priceMax?: number
|
||||
condition?: string[]
|
||||
rarity?: number[]
|
||||
tags?: string[]
|
||||
sortBy?: 'date' | 'price-low' | 'price-high' | 'rarity' | 'popularity'
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
items: CuratedItem[]
|
||||
total: number
|
||||
page: number
|
||||
hasMore: boolean
|
||||
filters: SearchFilters
|
||||
}
|
||||
|
||||
// Newsletter Types
|
||||
export interface NewsletterSubscriber {
|
||||
id: string
|
||||
email: string
|
||||
status: 'active' | 'pending' | 'unsubscribed'
|
||||
segments: string[] // ['photography', 'music', 'books', 'design']
|
||||
subscribedAt: Date
|
||||
preferences: {
|
||||
frequency: 'daily' | 'weekly' | 'monthly'
|
||||
categories: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// Price Alert Types
|
||||
export interface PriceAlert {
|
||||
id: string
|
||||
userId: string
|
||||
itemId: string
|
||||
targetPrice: number
|
||||
isActive: boolean
|
||||
createdAt: Date
|
||||
triggeredAt?: Date
|
||||
}
|
||||
|
||||
// Activity & Analytics Types
|
||||
export interface UserActivity {
|
||||
id: string
|
||||
userId: string
|
||||
type: 'view' | 'wishlist' | 'share' | 'click' | 'search'
|
||||
itemId?: string
|
||||
metadata?: Record<string, any>
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
// API Response Types
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
// Component Props Types
|
||||
export interface ItemCardProps {
|
||||
item: CuratedItem
|
||||
showWishlist?: boolean
|
||||
showExpertNotes?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export interface SearchBarProps {
|
||||
onSearch: (query: string) => void
|
||||
placeholder?: string
|
||||
showFilters?: boolean
|
||||
}
|
||||
|
||||
export interface WishlistButtonProps {
|
||||
itemId: string
|
||||
isWishlisted: boolean
|
||||
onToggle: (itemId: string) => void
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||