mirror of
https://github.com/freestylefly/awesome-gpt-image-2.git
synced 2026-09-14 14:00:36 +08:00
Add Vercel product gallery site
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
.vercel/
|
||||
npm-debug.log*
|
||||
.vercel
|
||||
+8025
File diff suppressed because one or more lines are too long
+16
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Explore GPT-Image2 prompt cases with copyable prompts, visual filters, and direct GitHub links."
|
||||
/>
|
||||
<title>GPT-Image2 Prompt Gallery</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1713
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "awesome-gpt-image-2-site",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "node scripts/generate-site-data.mjs",
|
||||
"dev": "vite",
|
||||
"prebuild": "node scripts/generate-site-data.mjs",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"vite": "^7.2.7",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"lucide-react": "^0.561.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const docsDir = join(root, 'docs');
|
||||
const outFile = join(root, 'data', 'cases.json');
|
||||
|
||||
const galleryFiles = [
|
||||
{ file: 'gallery-part-1.md', part: 1 },
|
||||
{ file: 'gallery-part-2.md', part: 2 }
|
||||
];
|
||||
|
||||
const categoryLabels = {
|
||||
'cat-ui': 'UI & Interfaces',
|
||||
'cat-infographic': 'Charts & Infographics',
|
||||
'cat-poster': 'Posters & Typography',
|
||||
'cat-product': 'Products & E-commerce',
|
||||
'cat-brand': 'Brand & Logos',
|
||||
'cat-architecture': 'Architecture & Spaces',
|
||||
'cat-photo': 'Photography & Realism',
|
||||
'cat-illustration': 'Illustration & Art',
|
||||
'cat-character': 'Characters & People',
|
||||
'cat-scene': 'Scenes & Storytelling',
|
||||
'cat-history': 'History & Classical Themes',
|
||||
'cat-document': 'Documents & Publishing',
|
||||
'cat-other': 'Other Use Cases'
|
||||
};
|
||||
|
||||
const featuredIds = new Set([
|
||||
1, 2, 6, 17, 166, 310, 330, 334, 338, 341, 344, 346, 350, 353, 354, 359, 360,
|
||||
361, 362, 365, 370, 373, 375, 376, 377, 378
|
||||
]);
|
||||
|
||||
function cleanText(value = '') {
|
||||
return value
|
||||
.replace(/\\_/g, '_')
|
||||
.replace(/\r/g, '')
|
||||
.replace(/[ \t]+\n/g, '\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stripMarkdown(value = '') {
|
||||
return cleanText(value)
|
||||
.replace(/\*\*/g, '')
|
||||
.replace(/`/g, '')
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseCategoryMap() {
|
||||
const text = readFileSync(join(docsDir, 'gallery.md'), 'utf8');
|
||||
const map = new Map();
|
||||
const sections = text.split(/<a name="(cat-[^"]+)"><\/a>/g);
|
||||
|
||||
for (let i = 1; i < sections.length; i += 2) {
|
||||
const categoryId = sections[i];
|
||||
const body = sections[i + 1] || '';
|
||||
const category = categoryLabels[categoryId] || 'Other Use Cases';
|
||||
for (const match of body.matchAll(/#case-(\d+)\)/g)) {
|
||||
map.set(Number(match[1]), category);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function extractPrompt(block) {
|
||||
const normalized = block.replace(/\r/g, '');
|
||||
const match = normalized.match(/\*\*提示词:\*\*[\s\S]*?```(?:text)?\n([\s\S]*?)```/);
|
||||
return cleanText(match?.[1] || '');
|
||||
}
|
||||
|
||||
function extractSource(block) {
|
||||
const line = block.match(/\*\*来源:\*\*\s*([^\n]+)/)?.[1] || '';
|
||||
const link = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (link) {
|
||||
return {
|
||||
label: stripMarkdown(link[1]),
|
||||
url: link[2]
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: stripMarkdown(line) || 'Community',
|
||||
url: ''
|
||||
};
|
||||
}
|
||||
|
||||
function inferCategory(caseItem) {
|
||||
if (caseItem.category) return caseItem.category;
|
||||
const text = `${caseItem.title} ${caseItem.prompt}`.toLowerCase();
|
||||
const rules = [
|
||||
['UI & Interfaces', ['ui', 'app', 'interface', 'dashboard', 'screenshot', '网页', '界面', '截图']],
|
||||
['Charts & Infographics', ['infographic', 'diagram', 'chart', 'atlas', '图谱', '信息图', '图解']],
|
||||
['Posters & Typography', ['poster', 'cover', 'typography', '海报', '封面', '字体']],
|
||||
['Products & E-commerce', ['product', 'packaging', 'e-commerce', '商品', '电商', '包装']],
|
||||
['Brand & Logos', ['logo', 'brand', 'identity', '品牌', '标志']],
|
||||
['Architecture & Spaces', ['architecture', 'interior', 'map', '建筑', '室内', '地图']],
|
||||
['Photography & Realism', ['photo', 'portrait', 'camera', 'realistic', '写真', '摄影', '写实']],
|
||||
['Illustration & Art', ['illustration', 'painting', 'watercolor', '插画', '艺术', '水墨']],
|
||||
['Characters & People', ['character', 'pose', 'avatar', '角色', '人物', '头像']],
|
||||
['Scenes & Storytelling', ['storyboard', 'scene', 'narrative', '场景', '叙事', '分镜']],
|
||||
['History & Classical Themes', ['history', 'dynasty', 'classical', '历史', '古风', '唐朝', '宋']],
|
||||
['Documents & Publishing', ['document', 'manual', 'prescription', '文档', '手册', '处方']]
|
||||
];
|
||||
return rules.find(([, keys]) => keys.some((key) => text.includes(key)))?.[0] || 'Other Use Cases';
|
||||
}
|
||||
|
||||
function inferTags(caseItem) {
|
||||
const text = `${caseItem.title} ${caseItem.prompt}`.toLowerCase();
|
||||
const styleRules = [
|
||||
['UI', ['ui', 'interface', 'dashboard', '界面']],
|
||||
['Infographic', ['infographic', 'diagram', 'chart', '信息图', '图解']],
|
||||
['Poster', ['poster', 'cover', 'typography', '海报', '封面']],
|
||||
['Realistic', ['photo', 'realistic', 'camera', '写真', '写实']],
|
||||
['Illustration', ['illustration', 'painting', 'watercolor', '插画', '绘画']],
|
||||
['Product', ['product', 'packaging', '商品', '包装']],
|
||||
['Brand', ['brand', 'logo', '品牌', '标志']],
|
||||
['Character', ['character', 'avatar', 'pose', '角色', '人物']],
|
||||
['Classical', ['classical', 'dynasty', 'history', '古风', '历史']],
|
||||
['3D', ['3d', 'toy', 'render', '玩具']]
|
||||
];
|
||||
const sceneRules = [
|
||||
['Tech', ['ai', 'rag', 'tech', 'data', '技术', '数据']],
|
||||
['Commerce', ['product', 'brand', 'ad', 'campaign', '商品', '商业', '广告']],
|
||||
['Education', ['guide', 'atlas', 'science', 'learning', '学习', '科普']],
|
||||
['Social', ['social', 'x ', 'wechat', '朋友圈', '社媒']],
|
||||
['Fashion', ['fashion', 'clothing', 'portrait', '服饰', '写真']],
|
||||
['Food', ['food', 'drink', 'coffee', 'tea', '餐厅', '咖啡', '茶']],
|
||||
['Travel', ['city', 'map', 'street', '城市', '地图', '街头']],
|
||||
['Story', ['story', 'scene', 'world', '故事', '场景']],
|
||||
['History', ['history', 'dynasty', 'ancient', '历史', '古希腊', '唐']]
|
||||
];
|
||||
|
||||
const pick = (rules, fallback) => {
|
||||
const tags = rules
|
||||
.filter(([, keys]) => keys.some((key) => text.includes(key)))
|
||||
.map(([label]) => label);
|
||||
return tags.length ? tags.slice(0, 3) : [fallback];
|
||||
};
|
||||
|
||||
return {
|
||||
styles: pick(styleRules, caseItem.category.split(' & ')[0].replace('Posters', 'Poster')),
|
||||
scenes: pick(sceneRules, 'Creative')
|
||||
};
|
||||
}
|
||||
|
||||
function parseCases() {
|
||||
const categoryMap = parseCategoryMap();
|
||||
const cases = [];
|
||||
|
||||
for (const { file, part } of galleryFiles) {
|
||||
const text = readFileSync(join(docsDir, file), 'utf8');
|
||||
const chunks = text.split(/<a name="case-(\d+)"><\/a>/g);
|
||||
|
||||
for (let i = 1; i < chunks.length; i += 2) {
|
||||
const id = Number(chunks[i]);
|
||||
const block = chunks[i + 1] || '';
|
||||
const title = stripMarkdown(block.match(/###\s*例\s*\d+:([^\n]+)/)?.[1] || `Case ${id}`);
|
||||
const imageMatch = block.match(/!\[([^\]]*)\]\(([^)]+)\)/);
|
||||
const prompt = extractPrompt(block);
|
||||
const source = extractSource(block);
|
||||
const category = inferCategory({
|
||||
title,
|
||||
prompt,
|
||||
category: categoryMap.get(id)
|
||||
});
|
||||
const image = imageMatch?.[2]
|
||||
? imageMatch[2].replace('../data/', '/')
|
||||
: `/images/case${id}.jpg`;
|
||||
const tags = inferTags({ title, prompt, category });
|
||||
|
||||
cases.push({
|
||||
id,
|
||||
title,
|
||||
image,
|
||||
imageAlt: stripMarkdown(imageMatch?.[1] || title),
|
||||
sourceLabel: source.label,
|
||||
sourceUrl: source.url,
|
||||
prompt,
|
||||
promptPreview: prompt.replace(/\n+/g, ' ').slice(0, 220),
|
||||
category,
|
||||
styles: tags.styles,
|
||||
scenes: tags.scenes,
|
||||
featured: featuredIds.has(id),
|
||||
githubUrl: `https://github.com/freestylefly/awesome-gpt-image-2/blob/main/docs/gallery-part-${part}.md#case-${id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cases.sort((a, b) => b.id - a.id);
|
||||
}
|
||||
|
||||
const cases = parseCases();
|
||||
const categories = [...new Set(cases.map((item) => item.category))].sort();
|
||||
const styles = [...new Set(cases.flatMap((item) => item.styles))].sort();
|
||||
const scenes = [...new Set(cases.flatMap((item) => item.scenes))].sort();
|
||||
|
||||
const payload = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
repository: 'https://github.com/freestylefly/awesome-gpt-image-2',
|
||||
totalCases: cases.length,
|
||||
categories,
|
||||
styles,
|
||||
scenes,
|
||||
cases
|
||||
};
|
||||
|
||||
mkdirSync(dirname(outFile), { recursive: true });
|
||||
writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\n`);
|
||||
console.log(`Generated ${cases.length} cases at ${outFile}`);
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
Copy,
|
||||
Github,
|
||||
Search,
|
||||
Sparkles,
|
||||
WandSparkles
|
||||
} from 'lucide-react';
|
||||
import './styles.css';
|
||||
|
||||
const fallbackRepoUrl = 'https://github.com/freestylefly/awesome-gpt-image-2';
|
||||
|
||||
function cx(...classes) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function useCopy() {
|
||||
const [copiedId, setCopiedId] = useState(null);
|
||||
|
||||
async function copyPrompt(caseItem) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(caseItem.prompt);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = caseItem.prompt;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
setCopiedId(caseItem.id);
|
||||
window.setTimeout(() => setCopiedId(null), 1600);
|
||||
}
|
||||
|
||||
return { copiedId, copyPrompt };
|
||||
}
|
||||
|
||||
function Hero({ hotCases, repoUrl, totalCases, categoryCount }) {
|
||||
return (
|
||||
<section className="hero">
|
||||
<div className="heroGlow heroGlowA" />
|
||||
<div className="heroGlow heroGlowB" />
|
||||
<div className="scanGrid" />
|
||||
<div className="heroCopy">
|
||||
<div className="eyebrow">
|
||||
<Sparkles size={16} />
|
||||
Live GPT-Image2 prompt gallery
|
||||
</div>
|
||||
<h1>Browse viral GPT-Image2 cases like a product catalog.</h1>
|
||||
<p>
|
||||
A visual front door for the awesome-gpt-image-2 repository: copy production-ready prompts,
|
||||
filter by style or scene, and jump straight into the GitHub source.
|
||||
</p>
|
||||
<div className="heroActions">
|
||||
<a className="primaryAction" href="#gallery">
|
||||
Explore cases
|
||||
<ArrowUpRight size={18} />
|
||||
</a>
|
||||
<a className="secondaryAction" href={repoUrl} target="_blank" rel="noreferrer">
|
||||
<Github size={18} />
|
||||
GitHub project
|
||||
</a>
|
||||
</div>
|
||||
<div className="metrics">
|
||||
<span><strong>{totalCases}</strong> cases</span>
|
||||
<span><strong>{categoryCount}</strong> categories</span>
|
||||
<span><strong>20+</strong> templates</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="heroDeck" aria-label="Featured GPT-Image2 cases">
|
||||
{hotCases.slice(0, 5).map((caseItem, index) => (
|
||||
<a
|
||||
className={`heroCard heroCard${index + 1}`}
|
||||
href={caseItem.githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
key={caseItem.id}
|
||||
>
|
||||
<img src={caseItem.image} alt={caseItem.imageAlt} />
|
||||
<span>Case {caseItem.id}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterPill({ active, children, onClick }) {
|
||||
return (
|
||||
<button className={cx('filterPill', active && 'active')} type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptCard({ caseItem, copied, onCopy }) {
|
||||
return (
|
||||
<article className="caseCard">
|
||||
<a className="caseImage" href={caseItem.githubUrl} target="_blank" rel="noreferrer">
|
||||
<img src={caseItem.image} alt={caseItem.imageAlt} loading="lazy" />
|
||||
<span className="caseBadge">Case {caseItem.id}</span>
|
||||
</a>
|
||||
<div className="caseBody">
|
||||
<div className="caseMeta">
|
||||
<span>{caseItem.category}</span>
|
||||
{caseItem.sourceUrl ? (
|
||||
<a href={caseItem.sourceUrl} target="_blank" rel="noreferrer">
|
||||
{caseItem.sourceLabel}
|
||||
</a>
|
||||
) : (
|
||||
<span>{caseItem.sourceLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
<h3>{caseItem.title}</h3>
|
||||
<p>{caseItem.promptPreview}</p>
|
||||
<div className="tagRow">
|
||||
{[...new Set([...caseItem.styles, ...caseItem.scenes])].slice(0, 4).map((tag) => (
|
||||
<span key={`${caseItem.id}-${tag}`}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="cardActions">
|
||||
<button type="button" onClick={() => onCopy(caseItem)}>
|
||||
{copied ? <Check size={17} /> : <Copy size={17} />}
|
||||
{copied ? 'Copied' : 'Copy Prompt'}
|
||||
</button>
|
||||
<a href={caseItem.githubUrl} target="_blank" rel="noreferrer" aria-label="Open on GitHub">
|
||||
<Github size={18} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [siteData, setSiteData] = useState(null);
|
||||
const [query, setQuery] = useState('');
|
||||
const [category, setCategory] = useState('All');
|
||||
const [style, setStyle] = useState('All');
|
||||
const [scene, setScene] = useState('All');
|
||||
const { copiedId, copyPrompt } = useCopy();
|
||||
const repoUrl = siteData?.repository || fallbackRepoUrl;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch('/cases.json')
|
||||
.then((response) => response.json())
|
||||
.then((payload) => {
|
||||
if (!cancelled) setSiteData(payload);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const hotCases = useMemo(() => {
|
||||
if (!siteData) return [];
|
||||
return [...siteData.cases]
|
||||
.filter((item) => item.featured)
|
||||
.sort((a, b) => b.id - a.id);
|
||||
}, [siteData]);
|
||||
|
||||
const filteredCases = useMemo(() => {
|
||||
if (!siteData) return [];
|
||||
const q = query.trim().toLowerCase();
|
||||
return siteData.cases.filter((item) => {
|
||||
const matchQuery =
|
||||
!q ||
|
||||
`${item.id} ${item.title} ${item.category} ${item.prompt} ${item.sourceLabel}`
|
||||
.toLowerCase()
|
||||
.includes(q);
|
||||
const matchCategory = category === 'All' || item.category === category;
|
||||
const matchStyle = style === 'All' || item.styles.includes(style);
|
||||
const matchScene = scene === 'All' || item.scenes.includes(scene);
|
||||
return matchQuery && matchCategory && matchStyle && matchScene;
|
||||
});
|
||||
}, [siteData, query, category, style, scene]);
|
||||
|
||||
const visibleCases = filteredCases.slice(0, 72);
|
||||
|
||||
if (!siteData) {
|
||||
return (
|
||||
<main>
|
||||
<div className="loadingScreen">
|
||||
<WandSparkles size={28} />
|
||||
<span>Loading GPT-Image2 cases...</span>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<header className="topbar">
|
||||
<a className="brand" href="#">
|
||||
<WandSparkles size={21} />
|
||||
GPT-Image2 Gallery
|
||||
</a>
|
||||
<nav>
|
||||
<a href="#gallery">Cases</a>
|
||||
<a href={repoUrl} target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<Hero
|
||||
hotCases={hotCases}
|
||||
repoUrl={repoUrl}
|
||||
totalCases={siteData.totalCases}
|
||||
categoryCount={siteData.categories.length}
|
||||
/>
|
||||
|
||||
<section className="hotStrip">
|
||||
{hotCases.slice(0, 8).map((caseItem) => (
|
||||
<a href={caseItem.githubUrl} target="_blank" rel="noreferrer" key={caseItem.id}>
|
||||
<img src={caseItem.image} alt={caseItem.imageAlt} />
|
||||
<span>#{caseItem.id}</span>
|
||||
</a>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="gallerySection" id="gallery">
|
||||
<div className="sectionHead">
|
||||
<div>
|
||||
<span className="eyebrow">Copy, filter, remix</span>
|
||||
<h2>Viral cases with prompts one click away.</h2>
|
||||
</div>
|
||||
<div className="searchBox">
|
||||
<Search size={18} />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search cases, sources, prompts..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filterPanel">
|
||||
<div>
|
||||
<strong>Category</strong>
|
||||
<div className="filterRow">
|
||||
<FilterPill active={category === 'All'} onClick={() => setCategory('All')}>All</FilterPill>
|
||||
{siteData.categories.map((item) => (
|
||||
<FilterPill key={item} active={category === item} onClick={() => setCategory(item)}>
|
||||
{item}
|
||||
</FilterPill>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Style</strong>
|
||||
<div className="filterRow">
|
||||
<FilterPill active={style === 'All'} onClick={() => setStyle('All')}>All</FilterPill>
|
||||
{siteData.styles.map((item) => (
|
||||
<FilterPill key={item} active={style === item} onClick={() => setStyle(item)}>
|
||||
{item}
|
||||
</FilterPill>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Scene</strong>
|
||||
<div className="filterRow">
|
||||
<FilterPill active={scene === 'All'} onClick={() => setScene('All')}>All</FilterPill>
|
||||
{siteData.scenes.map((item) => (
|
||||
<FilterPill key={item} active={scene === item} onClick={() => setScene(item)}>
|
||||
{item}
|
||||
</FilterPill>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="resultBar">
|
||||
<span>{filteredCases.length} matching cases</span>
|
||||
<a href={repoUrl} target="_blank" rel="noreferrer">
|
||||
Open GitHub project
|
||||
<ArrowUpRight size={16} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="caseGrid">
|
||||
{visibleCases.map((caseItem) => (
|
||||
<PromptCard
|
||||
caseItem={caseItem}
|
||||
copied={copiedId === caseItem.id}
|
||||
onCopy={copyPrompt}
|
||||
key={caseItem.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredCases.length > visibleCases.length && (
|
||||
<p className="limitNote">
|
||||
Showing the first {visibleCases.length} results for speed. Use search or filters to narrow the gallery.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')).render(<App />);
|
||||
+724
@@ -0,0 +1,724 @@
|
||||
:root {
|
||||
color: #eef5ff;
|
||||
background: #060914;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: geometricPrecision;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at 12% 8%, rgba(47, 211, 255, 0.18), transparent 28rem),
|
||||
radial-gradient(circle at 88% 0%, rgba(255, 73, 167, 0.16), transparent 30rem),
|
||||
linear-gradient(180deg, #060914 0%, #0b1020 48%, #080b14 100%);
|
||||
color: #eef5ff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loadingScreen {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 14px;
|
||||
min-height: 100vh;
|
||||
color: #9eeeff;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.loadingScreen svg {
|
||||
animation: cardDrift 2.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
height: 72px;
|
||||
margin: 0 auto;
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.brand,
|
||||
.topbar nav,
|
||||
.eyebrow,
|
||||
.heroActions a,
|
||||
.metrics span,
|
||||
.searchBox,
|
||||
.filterPanel,
|
||||
.resultBar,
|
||||
.caseCard {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(9, 15, 32, 0.68);
|
||||
box-shadow: 0 20px 70px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.brand,
|
||||
.topbar nav {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.brand svg {
|
||||
color: #50e7ff;
|
||||
}
|
||||
|
||||
.topbar nav {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.topbar nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 7px;
|
||||
color: #b7c7df;
|
||||
}
|
||||
|
||||
.topbar nav a:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.78fr);
|
||||
gap: 44px;
|
||||
align-items: center;
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
min-height: calc(100vh - 92px);
|
||||
margin: 0 auto;
|
||||
padding: 56px 0 72px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heroGlow,
|
||||
.scanGrid {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.heroGlow {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
border-radius: 999px;
|
||||
filter: blur(20px);
|
||||
opacity: 0.42;
|
||||
animation: floatGlow 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.heroGlowA {
|
||||
left: -130px;
|
||||
top: 8%;
|
||||
background: radial-gradient(circle, rgba(68, 222, 255, 0.42), transparent 64%);
|
||||
}
|
||||
|
||||
.heroGlowB {
|
||||
right: -120px;
|
||||
bottom: 10%;
|
||||
background: radial-gradient(circle, rgba(255, 74, 166, 0.34), transparent 62%);
|
||||
animation-delay: -3s;
|
||||
}
|
||||
|
||||
.scanGrid {
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
|
||||
background-size: 58px 58px;
|
||||
mask-image: linear-gradient(90deg, #000 0%, transparent 72%);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.heroCopy,
|
||||
.heroDeck {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
color: #9eeeff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
max-width: 780px;
|
||||
margin: 22px 0 20px;
|
||||
font-size: clamp(52px, 8vw, 92px);
|
||||
line-height: 0.94;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 650px;
|
||||
margin: 0;
|
||||
color: #b9c6d8;
|
||||
font-size: 18px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.heroActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
.heroActions a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
min-height: 48px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.heroActions .primaryAction {
|
||||
background: linear-gradient(135deg, #42e6ff, #78ffb9 52%, #f9ff72);
|
||||
color: #06101a;
|
||||
}
|
||||
|
||||
.heroActions .secondaryAction {
|
||||
color: #dfeaff;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.metrics span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
color: #aebcd0;
|
||||
}
|
||||
|
||||
.metrics strong {
|
||||
margin-right: 6px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.heroDeck {
|
||||
min-height: 610px;
|
||||
}
|
||||
|
||||
.heroCard {
|
||||
position: absolute;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 8px;
|
||||
background: #111a2c;
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.42);
|
||||
transform: rotate(var(--tilt));
|
||||
transition: transform 220ms ease, border-color 220ms ease;
|
||||
animation: cardDrift 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.heroCard:hover {
|
||||
border-color: rgba(103, 232, 249, 0.8);
|
||||
transform: translateY(-8px) rotate(var(--tilt));
|
||||
}
|
||||
|
||||
.heroCard img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.heroCard span {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
padding: 6px 9px;
|
||||
border-radius: 7px;
|
||||
background: rgba(4, 9, 18, 0.72);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.heroCard1 {
|
||||
--tilt: -5deg;
|
||||
inset: 30px auto auto 42px;
|
||||
width: 285px;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.heroCard2 {
|
||||
--tilt: 4deg;
|
||||
inset: 0 8px auto auto;
|
||||
width: 240px;
|
||||
height: 310px;
|
||||
animation-delay: -1.2s;
|
||||
}
|
||||
|
||||
.heroCard3 {
|
||||
--tilt: 5deg;
|
||||
inset: 318px auto auto 5px;
|
||||
width: 220px;
|
||||
height: 250px;
|
||||
animation-delay: -2.3s;
|
||||
}
|
||||
|
||||
.heroCard4 {
|
||||
--tilt: -3deg;
|
||||
inset: 300px 36px auto auto;
|
||||
width: 300px;
|
||||
height: 270px;
|
||||
animation-delay: -3.4s;
|
||||
}
|
||||
|
||||
.heroCard5 {
|
||||
--tilt: 2deg;
|
||||
inset: 196px auto auto 190px;
|
||||
width: 210px;
|
||||
height: 260px;
|
||||
animation-delay: -4.2s;
|
||||
}
|
||||
|
||||
.hotStrip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 0 auto 84px;
|
||||
}
|
||||
|
||||
.hotStrip a {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.hotStrip img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
.hotStrip a:hover img {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.hotStrip span {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(4, 9, 18, 0.74);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.gallerySection {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 400px);
|
||||
gap: 24px;
|
||||
align-items: end;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.sectionHead h2 {
|
||||
max-width: 760px;
|
||||
margin: 16px 0 0;
|
||||
font-size: clamp(34px, 5vw, 58px);
|
||||
line-height: 1.04;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.searchBox svg {
|
||||
color: #7de8ff;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.searchBox input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.searchBox input::placeholder {
|
||||
color: #73859f;
|
||||
}
|
||||
|
||||
.filterPanel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 18px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filterPanel strong {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #eaf3ff;
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filterPill {
|
||||
min-height: 34px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: 999px;
|
||||
padding: 0 12px;
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
color: #aebcd0;
|
||||
}
|
||||
|
||||
.filterPill:hover,
|
||||
.filterPill.active {
|
||||
border-color: rgba(103, 232, 249, 0.82);
|
||||
background: rgba(103, 232, 249, 0.16);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.resultBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
min-height: 46px;
|
||||
margin: 18px 0;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
color: #aebcd0;
|
||||
}
|
||||
|
||||
.resultBar a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #aef6ff;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.caseGrid {
|
||||
columns: 3 280px;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
.caseCard {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0 0 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.caseCard:hover {
|
||||
border-color: rgba(103, 232, 249, 0.54);
|
||||
background: rgba(14, 22, 42, 0.86);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.caseImage {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.caseImage img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.caseBadge {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
padding: 6px 9px;
|
||||
border-radius: 7px;
|
||||
background: rgba(4, 9, 18, 0.74);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.caseBody {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.caseMeta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: #7feaff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.caseMeta a {
|
||||
color: #c8ffb8;
|
||||
}
|
||||
|
||||
.caseBody h3 {
|
||||
margin: 10px 0 8px;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.caseBody p {
|
||||
display: -webkit-box;
|
||||
min-height: 76px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #aebcd0;
|
||||
line-height: 1.55;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.tagRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.tagRow span {
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
color: #c8d4e8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cardActions button,
|
||||
.cardActions a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.cardActions button:hover,
|
||||
.cardActions a:hover {
|
||||
border-color: rgba(120, 255, 185, 0.74);
|
||||
background: rgba(120, 255, 185, 0.14);
|
||||
}
|
||||
|
||||
.limitNote {
|
||||
margin: 24px 0 0;
|
||||
color: #8d9bb0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes floatGlow {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate3d(24px, -18px, 0) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cardDrift {
|
||||
0%,
|
||||
100% {
|
||||
translate: 0 0;
|
||||
}
|
||||
50% {
|
||||
translate: 0 -12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.heroDeck {
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.heroCard1 {
|
||||
left: 0;
|
||||
width: 48%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.heroCard2 {
|
||||
right: 0;
|
||||
width: 44%;
|
||||
height: 255px;
|
||||
}
|
||||
|
||||
.heroCard3 {
|
||||
top: 250px;
|
||||
width: 38%;
|
||||
height: 170px;
|
||||
}
|
||||
|
||||
.heroCard4 {
|
||||
top: 230px;
|
||||
right: 0;
|
||||
width: 52%;
|
||||
height: 190px;
|
||||
}
|
||||
|
||||
.heroCard5 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hotStrip {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.topbar {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
max-width: 205px;
|
||||
}
|
||||
|
||||
.topbar nav a {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
width: min(100% - 24px, 1180px);
|
||||
padding-top: 34px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.heroActions a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metrics span {
|
||||
flex: 1 1 130px;
|
||||
}
|
||||
|
||||
.hotStrip,
|
||||
.gallerySection {
|
||||
width: min(100% - 24px, 1180px);
|
||||
}
|
||||
|
||||
.hotStrip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-bottom: 58px;
|
||||
}
|
||||
|
||||
.resultBar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"framework": "vite",
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"cleanUrls": true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
publicDir: 'data',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user