Skip to content

Commit a824cd5

Browse files
Mile-Awayclaude
andcommitted
feat(web): redesign login page, navbar dark theme, product showcase update
- Login: Xyzen-style OAuth login (Apple, GitHub, Google, Bohrium) with dark gradient, motion animations, i18n - Callback: dark theme with proper i18n error/success states - Navbar: single dark theme (white text), hide ThemeToggle, keep LangSwitch - Products: first-party (Science Node, Eyes, Dongle) + third-party ecosystem section - i18n: login keys merged into translation namespace, product keys updated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df03a4e commit a824cd5

13 files changed

Lines changed: 325 additions & 298 deletions

File tree

2.12 KB
Loading

‎web/src/app/landscape/ProductShowcase.tsx‎

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { motion } from 'framer-motion';
22
import { useTranslation } from 'react-i18next';
33

4-
const products = [
4+
const firstParty = [
5+
{ id: 'science-node', nameKey: 'science_node', subtitleKey: 'science_node_subtitle', descKey: 'science_node_description', statusKey: 'status_available', highlight: true },
6+
{ id: 'eyes', nameKey: 'eyes', subtitleKey: 'eyes_subtitle', descKey: 'eyes_description', statusKey: 'status_available', highlight: false },
7+
{ id: 'dongle', nameKey: 'dongle', subtitleKey: 'dongle_subtitle', descKey: 'dongle_description', statusKey: 'status_available', highlight: false },
8+
];
9+
10+
const thirdParty = [
511
{ id: 'syringe-pump', nameKey: 'syringe_pump', subtitleKey: 'syringe_pump_subtitle', statusKey: 'status_available' },
612
{ id: 'liquid-handler', nameKey: 'liquid_handler', subtitleKey: 'liquid_handler_subtitle', statusKey: 'status_available' },
713
{ id: 'xyz-stage', nameKey: 'xyz_stage', subtitleKey: 'xyz_stage_subtitle', statusKey: 'status_coming_soon' },
@@ -31,8 +37,8 @@ export default function ProductShowcase() {
3137
</p>
3238
</motion.div>
3339

34-
<div className="mt-20 grid grid-cols-1 gap-4 sm:grid-cols-2">
35-
{products.map((product, i) => {
40+
<div className="mt-20 grid grid-cols-1 gap-4 lg:grid-cols-2">
41+
{firstParty.map((product, i) => {
3642
const isAvailable = product.statusKey === 'status_available';
3743
return (
3844
<motion.div
@@ -41,27 +47,32 @@ export default function ProductShowcase() {
4147
whileInView={{ opacity: 1, y: 0 }}
4248
viewport={{ once: true }}
4349
transition={{ duration: 0.6, delay: i * 0.1 }}
44-
className="group relative aspect-[4/3] overflow-hidden rounded-2xl bg-white/[0.02] ring-1 ring-white/[0.06] transition-all hover:ring-white/[0.12] cursor-pointer"
50+
className={`group relative overflow-hidden rounded-2xl bg-white/[0.02] ring-1 ring-white/[0.06] transition-all hover:ring-white/[0.12] cursor-pointer ${
51+
product.highlight ? 'lg:col-span-2 aspect-[21/9]' : 'aspect-[4/3]'
52+
}`}
4553
>
4654
<div className="absolute inset-0 flex items-center justify-center">
4755
<div className="relative">
48-
<div className="h-24 w-24 rounded-2xl border border-white/[0.06] rotate-12 transition-transform group-hover:rotate-6" />
49-
<div className="absolute inset-0 h-24 w-24 rounded-2xl border border-white/[0.04] -rotate-6 transition-transform group-hover:rotate-0" />
56+
<div className={`rounded-2xl border border-white/[0.06] rotate-12 transition-transform group-hover:rotate-6 ${product.highlight ? 'h-32 w-32' : 'h-24 w-24'}`} />
57+
<div className={`absolute inset-0 rounded-2xl border border-white/[0.04] -rotate-6 transition-transform group-hover:rotate-0 ${product.highlight ? 'h-32 w-32' : 'h-24 w-24'}`} />
5058
</div>
5159
</div>
5260

5361
<div className="absolute inset-x-0 bottom-0 p-6 bg-gradient-to-t from-black/80 via-black/40 to-transparent">
54-
<div className="flex items-end justify-between">
55-
<div>
56-
<h3 className="text-lg font-semibold text-white">
62+
<div className="flex items-end justify-between gap-4">
63+
<div className="flex-1">
64+
<h3 className={`font-semibold text-white ${product.highlight ? 'text-xl' : 'text-lg'}`}>
5765
{t(`landing.products.${product.nameKey}`)}
5866
</h3>
5967
<p className="mt-0.5 text-sm text-white/40 font-mono">
6068
{t(`landing.products.${product.subtitleKey}`)}
6169
</p>
70+
<p className="mt-2 text-sm text-white/25 leading-relaxed max-w-lg">
71+
{t(`landing.products.${product.descKey}`)}
72+
</p>
6273
</div>
6374
<span
64-
className={`rounded-full px-3 py-1 text-[11px] font-mono uppercase tracking-wider ${
75+
className={`shrink-0 rounded-full px-3 py-1 text-[11px] font-mono uppercase tracking-wider ${
6576
isAvailable
6677
? 'bg-white/[0.08] text-white/60 ring-1 ring-white/[0.1]'
6778
: 'bg-white/[0.03] text-white/25 ring-1 ring-white/[0.05]'
@@ -76,6 +87,54 @@ export default function ProductShowcase() {
7687
})}
7788
</div>
7889

90+
{/* Third-party compatible devices */}
91+
<motion.div
92+
initial={{ opacity: 0, y: 30 }}
93+
whileInView={{ opacity: 1, y: 0 }}
94+
viewport={{ once: true }}
95+
transition={{ duration: 0.7 }}
96+
className="mt-24"
97+
>
98+
<p className="font-mono text-xs uppercase tracking-[0.2em] text-white/30">
99+
{t('landing.products.third_party_label')}
100+
</p>
101+
<h3 className="mt-4 text-2xl sm:text-3xl font-bold tracking-tight text-white">
102+
{t('landing.products.third_party_title')}
103+
</h3>
104+
</motion.div>
105+
106+
<div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
107+
{thirdParty.map((product, i) => {
108+
const isAvailable = product.statusKey === 'status_available';
109+
return (
110+
<motion.div
111+
key={product.id}
112+
initial={{ opacity: 0, y: 20 }}
113+
whileInView={{ opacity: 1, y: 0 }}
114+
viewport={{ once: true }}
115+
transition={{ duration: 0.5, delay: i * 0.08 }}
116+
className="group relative overflow-hidden rounded-xl bg-white/[0.02] ring-1 ring-white/[0.06] transition-all hover:ring-white/[0.12] cursor-pointer p-5"
117+
>
118+
<h4 className="text-sm font-semibold text-white">
119+
{t(`landing.products.${product.nameKey}`)}
120+
</h4>
121+
<p className="mt-1 text-xs text-white/35 font-mono">
122+
{t(`landing.products.${product.subtitleKey}`)}
123+
</p>
124+
<span
125+
className={`mt-4 inline-block rounded-full px-2.5 py-0.5 text-[10px] font-mono uppercase tracking-wider ${
126+
isAvailable
127+
? 'bg-white/[0.06] text-white/50 ring-1 ring-white/[0.08]'
128+
: 'bg-white/[0.03] text-white/20 ring-1 ring-white/[0.04]'
129+
}`}
130+
>
131+
{t(`landing.products.${product.statusKey}`)}
132+
</span>
133+
</motion.div>
134+
);
135+
})}
136+
</div>
137+
79138
<motion.p
80139
initial={{ opacity: 0 }}
81140
whileInView={{ opacity: 1 }}

0 commit comments

Comments
 (0)