-
Notifications
You must be signed in to change notification settings - Fork 25
feat(app-store): redesign App Store pages #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
2fa7570
Redesign app store landing page
Che-Zhu b842ed3
Use home footer on app store page
Che-Zhu 3ef2075
Redesign app store pages
Che-Zhu dfbb052
Fix app store FAQ arrow state
Che-Zhu fd7317a
Fix app detail deploy diagram background
Che-Zhu a7a4189
Fix app detail live status icon
Che-Zhu 10ff287
Align app detail deploy step icons
Che-Zhu 1a34016
Show related template categories
Che-Zhu a4d8180
Tighten app detail section spacing
Che-Zhu 2c2509b
feat: render app readme previews from GitHub
Che-Zhu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
app/[lang]/products/app-store/[slug]/app-store-detail-page.test.mts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import test from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import { readFileSync } from 'node:fs'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const source = readFileSync( | ||
| join(dirname(fileURLToPath(import.meta.url)), 'page.tsx'), | ||
| 'utf8', | ||
| ); | ||
|
|
||
| test('app store detail page uses the redesigned dark shell and home footer', () => { | ||
| assert.match(source, /import \{ Footer \} from '@\/new-components\/Footer'/); | ||
| assert.match(source, /BottomLightImage/); | ||
| assert.match(source, /appStoreDetailBackgroundVars/); | ||
| assert.match(source, /data-theme="app-store"/); | ||
| assert.match(source, /className="[^"]*isolate[^"]*"/); | ||
| assert.doesNotMatch(source, /import Footer from '@\/components\/footer'/); | ||
| assert.doesNotMatch(source, /@\/components\/header\/hero/); | ||
| }); | ||
|
|
||
| test('app store detail page renders redesigned Figma sections', () => { | ||
| assert.match(source, /AppDetailHero/); | ||
| assert.match(source, /WhyDeployOnSealos/); | ||
| assert.match(source, /WholeStackSection/); | ||
| assert.match(source, /ReadmePreview/); | ||
| assert.match(source, /RelatedTemplates/); | ||
| }); |
228 changes: 228 additions & 0 deletions
228
app/[lang]/products/app-store/[slug]/components/AppDetailHero.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| import Link from 'next/link'; | ||
| import { | ||
| ArrowLeft, | ||
| ArrowRight, | ||
| BookOpen, | ||
| CircleCheck, | ||
| ExternalLink, | ||
| Github, | ||
| } from 'lucide-react'; | ||
| import { GodRays } from '@/new-components/GodRays'; | ||
| import { GradientLucideIcon } from '@/new-components/GradientLucideIcon'; | ||
| import { AppIcon } from '@/components/ui/app-icon'; | ||
| import { languagesType } from '@/lib/i18n'; | ||
| import { cn } from '@/lib/utils'; | ||
| import { DeployButton } from './DeployButton'; | ||
| import AppPreviewPanel from './AppPreviewPanel'; | ||
| import { | ||
| getDisplayDescription, | ||
| getReadmeUrl, | ||
| type AppDetailConfig, | ||
| } from './app-detail-utils'; | ||
|
|
||
| interface AppDetailHeroProps { | ||
| app: AppDetailConfig; | ||
| lang: languagesType; | ||
| } | ||
|
|
||
| const proofPoints = [ | ||
| 'Easy to deploy and manage', | ||
| 'Self-hosted solution', | ||
| 'Open source and free', | ||
| 'Community supported', | ||
| ]; | ||
|
|
||
| const heroTitleAccentClassName = | ||
| 'bg-gradient-to-r from-white to-[#146DFF] bg-clip-text text-transparent'; | ||
|
|
||
| const heroCenterLogoClassName = | ||
| 'hidden lg:flex absolute left-[609px] top-9 z-20 h-[130px] w-[130px] -translate-x-1/2 items-center justify-center rounded-[10px] border-[7px] border-[#25272c] bg-[#d8d8d8] p-5 shadow-2xl shadow-black/40'; | ||
|
|
||
| function textLinkClassName(variant: 'primary' | 'default' = 'default') { | ||
| return cn( | ||
| 'inline-flex items-center gap-1.5 text-xs transition', | ||
| variant === 'primary' | ||
| ? 'text-white hover:text-[#69a3ff]' | ||
| : 'text-zinc-400 hover:text-white', | ||
| ); | ||
| } | ||
|
|
||
| export default function AppDetailHero({ app, lang }: AppDetailHeroProps) { | ||
| const readmeUrl = getReadmeUrl(app.github); | ||
|
|
||
| return ( | ||
| <section className="relative overflow-hidden pt-36 pb-16 md:pt-44 md:pb-20 lg:pt-[132px] lg:pb-20"> | ||
| <div | ||
| className="pointer-events-none absolute inset-0 z-0 bg-background" | ||
| aria-hidden="true" | ||
| /> | ||
| <div className="pointer-events-none absolute inset-0 z-[3]"> | ||
| <GodRays | ||
| sources={[ | ||
| { | ||
| x: -0.05, | ||
| y: -0.05, | ||
| angle: 60, | ||
| spread: 20, | ||
| count: 12, | ||
| color: '220, 220, 220', | ||
| opacityMin: 0.24, | ||
| opacityMax: 0.25, | ||
| minWidth: 120, | ||
| maxWidth: 180, | ||
| }, | ||
| { | ||
| x: -0.05, | ||
| y: -0.05, | ||
| angle: 60, | ||
| spread: 8, | ||
| count: 6, | ||
| color: '255, 255, 255', | ||
| opacityMin: 0.89, | ||
| opacityMax: 0.9, | ||
| minWidth: 12, | ||
| maxWidth: 24, | ||
| }, | ||
| { | ||
| x: 0.25, | ||
| y: -0.06, | ||
| angle: 50, | ||
| spread: 20, | ||
| count: 6, | ||
| color: '180, 180, 180', | ||
| opacityMin: 0.14, | ||
| opacityMax: 0.15, | ||
| minWidth: 60, | ||
| maxWidth: 120, | ||
| }, | ||
| ]} | ||
| speed={0} | ||
| maxWidth={48} | ||
| minLength={1200} | ||
| maxLength={2000} | ||
| blur={8} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="relative z-10 mx-auto grid max-w-7xl items-start gap-12 px-6 lg:grid-cols-[minmax(0,560px)_minmax(0,1fr)] lg:px-8"> | ||
| <div className={heroCenterLogoClassName}> | ||
| <AppIcon | ||
| src={app.icon} | ||
| alt={`${app.name} icon`} | ||
| width={92} | ||
| height={92} | ||
| className="h-[92px] w-[92px] rounded-md object-contain" | ||
| fallbackClassName="h-16 w-16 text-zinc-700" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="relative z-20 min-w-0 lg:pt-2"> | ||
| <Link | ||
| href={`/${lang}/products/app-store`} | ||
| className="mb-12 inline-flex items-center gap-2 text-xs text-zinc-500 transition hover:text-zinc-200 lg:mb-[92px]" | ||
| > | ||
| <ArrowLeft className="h-3.5 w-3.5" /> | ||
| Back to templates | ||
| </Link> | ||
|
|
||
| <div className="mb-7 flex items-center gap-5 lg:hidden"> | ||
| <div className="flex h-[84px] w-[84px] items-center justify-center rounded-xl border border-white/15 bg-zinc-100 p-3 shadow-2xl shadow-black/40"> | ||
| <AppIcon | ||
| src={app.icon} | ||
| alt={`${app.name} icon`} | ||
| width={64} | ||
| height={64} | ||
| className="h-16 w-16 rounded-lg object-contain" | ||
| fallbackClassName="h-12 w-12 text-zinc-700" | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <h1 className="max-w-[720px] text-[34px] leading-[1.12] font-semibold text-white sm:text-[36px] lg:max-w-[500px] lg:text-[36px] lg:leading-[1.18]"> | ||
| Deploy{' '} | ||
| <span className={heroTitleAccentClassName}> | ||
| {app.name} on Sealos | ||
| </span> | ||
| </h1> | ||
| <p className="mt-6 max-w-[600px] text-sm leading-6 text-zinc-400"> | ||
| {getDisplayDescription(app)} | ||
| </p> | ||
|
|
||
| <div className="mt-6 flex flex-wrap items-center gap-2"> | ||
| <span className="inline-flex h-6 items-center gap-1 rounded-full bg-white/[0.055] px-2 text-xs text-zinc-400"> | ||
| <span className="h-1.5 w-1.5 rounded-full bg-[#6ea2ff]" /> | ||
| {app.category} | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className="mt-5 flex flex-wrap gap-x-5 gap-y-3 text-xs text-zinc-400"> | ||
| {(app.benefits.length ? app.benefits : proofPoints) | ||
| .slice(0, 4) | ||
| .map((item) => ( | ||
| <span key={item} className="inline-flex items-center gap-2"> | ||
| <GradientLucideIcon | ||
| Icon={CircleCheck} | ||
| className="h-3.5 w-3.5 shrink-0" | ||
| /> | ||
| {item} | ||
| </span> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className="mt-10"> | ||
| <DeployButton | ||
| templateName={app.slug} | ||
| appName={app.name} | ||
| category={app.category} | ||
| className="h-10 gap-2 px-6 text-sm" | ||
| > | ||
| Deploy Now | ||
| <ArrowRight className="h-4 w-4" /> | ||
| </DeployButton> | ||
| </div> | ||
|
|
||
| <div className="mt-9 flex flex-wrap items-center gap-5"> | ||
| {app.website && ( | ||
| <a | ||
| href={app.website} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className={textLinkClassName('primary')} | ||
| > | ||
| <ExternalLink className="h-3.5 w-3.5" /> | ||
| Official Website | ||
| </a> | ||
| )} | ||
| {app.github && ( | ||
| <a | ||
| href={app.github} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className={textLinkClassName()} | ||
| > | ||
| <Github className="h-3.5 w-3.5" /> | ||
| GitHub | ||
| </a> | ||
| )} | ||
| <a | ||
| href={readmeUrl} | ||
| target={app.github ? '_blank' : undefined} | ||
| rel={app.github ? 'noopener noreferrer' : undefined} | ||
| className={textLinkClassName()} | ||
| > | ||
| <BookOpen className="h-3.5 w-3.5" /> | ||
| README | ||
| </a> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="relative z-20 hidden min-w-0 lg:block lg:pt-[150px]"> | ||
| <div className="absolute -inset-10 bg-[radial-gradient(circle_at_45%_40%,rgba(20,109,255,0.18),transparent_54%)]" /> | ||
| <div className="relative translate-x-14"> | ||
| <AppPreviewPanel app={app} variant="hero" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ); | ||
| } | ||
133 changes: 133 additions & 0 deletions
133
app/[lang]/products/app-store/[slug]/components/AppPreviewPanel.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import Image from 'next/image'; | ||
| import { Globe } from 'lucide-react'; | ||
| import { AppIcon } from '@/components/ui/app-icon'; | ||
| import type { AppDetailConfig } from './app-detail-utils'; | ||
|
|
||
| interface AppPreviewPanelProps { | ||
| app: AppDetailConfig; | ||
| compact?: boolean; | ||
| displayUrl?: string; | ||
| showChrome?: boolean; | ||
| variant?: 'default' | 'hero' | 'readme'; | ||
| } | ||
|
|
||
| export default function AppPreviewPanel({ | ||
| app, | ||
| compact = false, | ||
| displayUrl, | ||
| showChrome = true, | ||
| variant = 'default', | ||
| }: AppPreviewPanelProps) { | ||
| const primaryScreenshot = app.screenshots?.[0]; | ||
| const readmePanelClassName = | ||
| 'relative aspect-[1253/671] min-h-[520px] overflow-hidden bg-[#d8d8d8] p-8 text-zinc-950 sm:p-10 lg:p-12'; | ||
| const heroPanelClassName = | ||
| 'relative aspect-[586/390] min-h-[300px] overflow-hidden bg-[#d8d8d8] p-6 text-zinc-950 sm:min-h-[340px] lg:p-8'; | ||
| const defaultPanelClassName = | ||
| 'relative min-h-[260px] overflow-hidden bg-[#d8d8d8] p-8 text-zinc-950 sm:min-h-[330px]'; | ||
| const contentMinHeightClassName = | ||
| variant === 'readme' | ||
| ? 'min-h-[456px] sm:min-h-[500px] lg:min-h-[575px]' | ||
| : variant === 'hero' | ||
| ? 'min-h-[252px] sm:min-h-[292px]' | ||
| : 'min-h-[220px] sm:min-h-[280px]'; | ||
| const heroTitleClassName = | ||
| 'max-w-[360px] text-[46px] leading-[0.98] font-semibold tracking-normal sm:text-[58px] lg:text-[66px]'; | ||
| const defaultTitleClassName = | ||
| 'text-[52px] leading-none font-semibold tracking-normal sm:text-[76px] lg:text-[96px]'; | ||
| const titleClassName = | ||
| variant === 'hero' ? heroTitleClassName : defaultTitleClassName; | ||
| const heroScreenshotImageClassName = | ||
| 'object-cover -translate-y-[5%] scale-[1.28]'; | ||
|
|
||
| return ( | ||
| <div className="relative overflow-hidden rounded-xl border border-white/10 bg-[#101218] shadow-2xl shadow-black/40"> | ||
| {showChrome && ( | ||
| <div className="flex h-8 items-center justify-between border-b border-white/10 bg-white/[0.035] px-3"> | ||
| <div className="flex items-center gap-1.5"> | ||
| <span className="h-2 w-2 rounded-full bg-[#ff5f57]" /> | ||
| <span className="h-2 w-2 rounded-full bg-[#ffbd2e]" /> | ||
| <span className="h-2 w-2 rounded-full bg-[#28c840]" /> | ||
| </div> | ||
| <div className="inline-flex items-center gap-1.5 rounded-md bg-white/[0.04] px-8 py-1 text-[10px] text-zinc-300"> | ||
| <Globe className="h-2.5 w-2.5 text-zinc-400" /> | ||
| sealos.io | ||
| </div> | ||
| <span className="w-[34px]" aria-hidden="true" /> | ||
| </div> | ||
| )} | ||
|
|
||
| {variant === 'hero' && primaryScreenshot ? ( | ||
| <div className="relative aspect-[586/390] min-h-[300px] overflow-hidden bg-[#d8d8d8] sm:min-h-[340px]"> | ||
| <Image | ||
| src={primaryScreenshot} | ||
| alt={`${app.name} screenshot`} | ||
| fill | ||
| className={heroScreenshotImageClassName} | ||
| sizes="(max-width: 1024px) 100vw, 586px" | ||
| priority | ||
| /> | ||
| </div> | ||
| ) : ( | ||
| <div | ||
| className={ | ||
| variant === 'readme' | ||
| ? readmePanelClassName | ||
| : variant === 'hero' | ||
| ? heroPanelClassName | ||
| : defaultPanelClassName | ||
| } | ||
| > | ||
| <div className="absolute inset-0 bg-[radial-gradient(circle_at_78%_28%,rgba(20,109,255,0.16),transparent_34%)]" /> | ||
| <div | ||
| className={`relative flex h-full flex-col justify-between ${contentMinHeightClassName}`} | ||
| > | ||
| <div className="flex items-start justify-between gap-6"> | ||
| <div> | ||
| <div className={titleClassName}>{app.name}</div> | ||
| <p className="mt-5 max-w-[410px] text-base leading-6 text-zinc-800 lg:text-lg lg:leading-7"> | ||
| Build production-ready {app.category.toLowerCase()} templates | ||
| with Sealos. | ||
| </p> | ||
| </div> | ||
| <div className="hidden shrink-0 rounded-2xl border border-black/10 bg-white/75 p-3 shadow-xl sm:block"> | ||
| <AppIcon | ||
| src={app.icon} | ||
| alt={`${app.name} icon`} | ||
| width={72} | ||
| height={72} | ||
| className="h-16 w-16 rounded-xl object-contain" | ||
| fallbackClassName="h-12 w-12 text-zinc-700" | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| {!compact && ( | ||
| <div className="grid gap-3 sm:grid-cols-3"> | ||
| {(app.features.length ? app.features : app.benefits) | ||
| .slice(0, 3) | ||
| .map((item) => ( | ||
| <div | ||
| key={item} | ||
| className="rounded-lg border border-black/10 bg-white/55 p-3 text-xs leading-5 text-zinc-700" | ||
| > | ||
| {item} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
|
|
||
| <div className="flex items-end justify-between gap-4"> | ||
| <span className="truncate text-base text-zinc-900"> | ||
| {displayUrl || app.website || app.github || `/${app.slug}`} | ||
| </span> | ||
| <span className="text-4xl leading-none font-semibold"> | ||
| deploy <span className="text-[#073edc]">now</span> | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The detail hero uses hardcoded English UI strings (for example the back link and CTA text) even though this route is language-scoped (
/[lang]/...). As a result, localized pages likezh-cnnow show English labels on the primary detail surface, which regresses the previous language-aware behavior and breaks locale consistency for production users.Useful? React with 👍 / 👎.