Fix lint
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E API Tests with external source of truth / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test With Custom Base Port / restart-dev-and-test-with-custom-base-port (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled

This commit is contained in:
Konstantin Wohlwend 2025-12-07 11:56:39 -08:00
parent 92ef462d13
commit 62b9fa3881

View File

@ -79,190 +79,190 @@ export function AppStoreEntry({
<div className="flex flex-col bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-950">
{/* Hero Section */}
<div className="relative px-6 py-8 border-b border-gray-200 dark:border-gray-800">
<div className="max-w-4xl mx-auto">
<div className="flex flex-col md:flex-row gap-6 items-start">
{/* App Icon */}
<div className="flex-shrink-0">
<AppIcon
appId={appId}
className="shadow-xl ring-1 ring-black/5 dark:ring-white/10"
/>
</div>
{/* App Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start gap-2 mb-2">
<TitleComponent className="text-3xl font-semibold text-gray-900 dark:text-gray-100">
{app.displayName}
</TitleComponent>
{app.stage !== "stable" && (
<Badge
variant={app.stage === "alpha" ? "destructive" : "secondary"}
className="text-xs px-2 py-0.5"
>
{app.stage === "alpha" ? "Alpha" : "Beta"}
</Badge>
)}
<div className="max-w-4xl mx-auto">
<div className="flex flex-col md:flex-row gap-6 items-start">
{/* App Icon */}
<div className="flex-shrink-0">
<AppIcon
appId={appId}
className="shadow-xl ring-1 ring-black/5 dark:ring-white/10"
/>
</div>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-4">
{app.subtitle}
</p>
{/* App Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start gap-2 mb-2">
<TitleComponent className="text-3xl font-semibold text-gray-900 dark:text-gray-100">
{app.displayName}
</TitleComponent>
{app.stage !== "stable" && (
<Badge
variant={app.stage === "alpha" ? "destructive" : "secondary"}
className="text-xs px-2 py-0.5"
>
{app.stage === "alpha" ? "Alpha" : "Beta"}
</Badge>
)}
</div>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-6">
{(app.tags as Array<keyof typeof ALL_APP_TAGS>).map((tag) => (
<div
key={tag}
className={cn(
<p className="text-lg text-gray-600 dark:text-gray-400 mb-4">
{app.subtitle}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-6">
{(app.tags as Array<keyof typeof ALL_APP_TAGS>).map((tag) => (
<div
key={tag}
className={cn(
"px-3 py-1 rounded-full text-xs font-medium",
tag === "expert"
? "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"
: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
)}
>
{ALL_APP_TAGS[tag].displayName}
</div>
))}
</div>
>
{ALL_APP_TAGS[tag].displayName}
</div>
))}
</div>
{/* Features Grid */}
<div className="grid grid-cols-3 gap-3 mb-6">
{features.map((feature, index) => (
<div
key={index}
className="flex flex-col items-center gap-1.5 p-3 rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800"
>
<feature.icon className="w-4 h-4 text-blue-500" />
<span className="text-xs text-gray-600 dark:text-gray-400 text-center">
{feature.label}
</span>
</div>
))}
</div>
{/* Features Grid */}
<div className="grid grid-cols-3 gap-3 mb-6">
{features.map((feature, index) => (
<div
key={index}
className="flex flex-col items-center gap-1.5 p-3 rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800"
>
<feature.icon className="w-4 h-4 text-blue-500" />
<span className="text-xs text-gray-600 dark:text-gray-400 text-center">
{feature.label}
</span>
</div>
))}
</div>
{/* CTA Button */}
<div className="flex items-center gap-4">
{isEnabled ? (
<>
{/* CTA Button */}
<div className="flex items-center gap-4">
{isEnabled ? (
<>
<Button
onClick={onOpen}
size="lg"
className="px-8 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium shadow-lg shadow-blue-500/20"
>
<ExternalLink className="w-4 h-4 mr-2" />
Open App
</Button>
{onDisable && (
<Button
onClick={onDisable}
variant="secondary"
size="sm"
>
Disable
</Button>
)}
</>
) : (
<Button
onClick={onOpen}
onClick={onEnable}
size="lg"
className="px-8 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium shadow-lg shadow-blue-500/20"
>
<ExternalLink className="w-4 h-4 mr-2" />
Open App
Enable App
</Button>
{onDisable && (
<Button
onClick={onDisable}
variant="secondary"
size="sm"
>
Disable
</Button>
)}
</>
) : (
<Button
onClick={onEnable}
size="lg"
className="px-8 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium shadow-lg shadow-blue-500/20"
>
Enable App
</Button>
)}
)}
</div>
</div>
</div>
</div>
</div>
{/* Stage Warning */}
{app.stage !== "stable" && (
<div className="max-w-4xl mx-auto mt-6">
<div
className={cn(
{/* Stage Warning */}
{app.stage !== "stable" && (
<div className="max-w-4xl mx-auto mt-6">
<div
className={cn(
"p-4 rounded-xl border-l-4",
app.stage === "alpha"
? "bg-red-50 dark:bg-red-950/20 border-red-500 dark:border-red-600"
: "bg-amber-50 dark:bg-amber-950/20 border-amber-500 dark:border-amber-600"
)}
>
<p
className={cn(
>
<p
className={cn(
"text-sm font-medium",
app.stage === "alpha"
? "text-red-800 dark:text-red-300"
: "text-amber-800 dark:text-amber-300"
)}
>
{app.stage === "alpha" && (
<>
<strong>Alpha Release:</strong> This app is in early development and may have bugs or unexpected behavior. Use with caution in production environments.
</>
)}
{app.stage === "beta" && (
<>
<strong>Beta Release:</strong> This app is being actively tested. You may encounter some issues, but it is generally stable for production use.
</>
)}
</p>
>
{app.stage === "alpha" && (
<>
<strong>Alpha Release:</strong> This app is in early development and may have bugs or unexpected behavior. Use with caution in production environments.
</>
)}
{app.stage === "beta" && (
<>
<strong>Beta Release:</strong> This app is being actively tested. You may encounter some issues, but it is generally stable for production use.
</>
)}
</p>
</div>
</div>
</div>
)}
</div>
)}
</div>
{/* Screenshots Section */}
{appFrontend.screenshots.length > 0 && (
<div className="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-6 py-8">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Preview
</h2>
<div className="relative group/screenshots">
{/* Left scroll button */}
<button
onClick={() => scrollScreenshots('left')}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 dark:bg-gray-900/90 hover:bg-white dark:hover:bg-gray-800 p-2 rounded-full shadow-lg border border-gray-200 dark:border-gray-700 opacity-0 group-hover/screenshots:opacity-100 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Scroll left"
>
<ChevronLeft className="w-5 h-5 text-gray-700 dark:text-gray-300" />
</button>
{/* Screenshots Section */}
{appFrontend.screenshots.length > 0 && (
<div className="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-6 py-8">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Preview
</h2>
<div className="relative group/screenshots">
{/* Left scroll button */}
<button
onClick={() => scrollScreenshots('left')}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 dark:bg-gray-900/90 hover:bg-white dark:hover:bg-gray-800 p-2 rounded-full shadow-lg border border-gray-200 dark:border-gray-700 opacity-0 group-hover/screenshots:opacity-100 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Scroll left"
>
<ChevronLeft className="w-5 h-5 text-gray-700 dark:text-gray-300" />
</button>
{/* Right scroll button */}
<button
onClick={() => scrollScreenshots('right')}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 dark:bg-gray-900/90 hover:bg-white dark:hover:bg-gray-800 p-2 rounded-full shadow-lg border border-gray-200 dark:border-gray-700 opacity-0 group-hover/screenshots:opacity-100 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Scroll right"
>
<ChevronRight className="w-5 h-5 text-gray-700 dark:text-gray-300" />
</button>
{/* Right scroll button */}
<button
onClick={() => scrollScreenshots('right')}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 dark:bg-gray-900/90 hover:bg-white dark:hover:bg-gray-800 p-2 rounded-full shadow-lg border border-gray-200 dark:border-gray-700 opacity-0 group-hover/screenshots:opacity-100 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Scroll right"
>
<ChevronRight className="w-5 h-5 text-gray-700 dark:text-gray-300" />
</button>
<div
ref={screenshotContainerRef}
className="flex gap-4 pb-4 overflow-x-auto scrollbar-hide scroll-smooth"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{appFrontend.screenshots.map((screenshot: string, index: number) => (
<button
key={index}
onClick={() => setPreviewIndex(index)}
className="relative h-64 w-96 rounded-xl shadow-lg flex-shrink-0 overflow-hidden border border-gray-200 dark:border-gray-800 cursor-pointer hover:ring-2 hover:ring-blue-500/50 transition-shadow focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<Image
src={screenshot}
alt={`${app.displayName} screenshot ${index + 1}`}
fill
className="object-cover select-none"
draggable={false}
/>
</button>
))}
<div
ref={screenshotContainerRef}
className="flex gap-4 pb-4 overflow-x-auto scrollbar-hide scroll-smooth"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{appFrontend.screenshots.map((screenshot: string, index: number) => (
<button
key={index}
onClick={() => setPreviewIndex(index)}
className="relative h-64 w-96 rounded-xl shadow-lg flex-shrink-0 overflow-hidden border border-gray-200 dark:border-gray-800 cursor-pointer hover:ring-2 hover:ring-blue-500/50 transition-shadow focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<Image
src={screenshot}
alt={`${app.displayName} screenshot ${index + 1}`}
fill
className="object-cover select-none"
draggable={false}
/>
</button>
))}
</div>
</div>
</div>
</div>
</div>
)}
)}
{/* Description Section */}
<div className="max-w-4xl mx-auto px-6 py-8">
@ -274,68 +274,68 @@ export function AppStoreEntry({
</div>
</div>
{/* Screenshot Preview Modal */}
<Dialog open={previewIndex !== null} onOpenChange={(open) => !open && setPreviewIndex(null)}>
<DialogContent className="max-w-7xl max-h-[95vh] p-0 bg-black/95 border-0" noCloseButton>
<DialogTitle className="sr-only">
{previewIndex !== null
? `${app.displayName} screenshot ${previewIndex + 1} of ${appFrontend.screenshots.length}`
: 'Screenshot preview'}
</DialogTitle>
<div className="relative w-full h-full flex items-center justify-center p-4">
{previewIndex !== null && (
<>
{/* Close button */}
<button
onClick={() => setPreviewIndex(null)}
className="absolute top-4 right-4 z-50 bg-white/10 hover:bg-white/20 p-2 rounded-full transition-colors"
aria-label="Close preview"
>
<X className="w-6 h-6 text-white" />
</button>
{/* Image counter */}
<div className="absolute top-4 left-4 z-50 bg-white/10 px-3 py-1 rounded-full text-white text-sm">
{previewIndex + 1} / {appFrontend.screenshots.length}
</div>
{/* Previous button */}
{previewIndex > 0 && (
{/* Screenshot Preview Modal */}
<Dialog open={previewIndex !== null} onOpenChange={(open) => !open && setPreviewIndex(null)}>
<DialogContent className="max-w-7xl max-h-[95vh] p-0 bg-black/95 border-0" noCloseButton>
<DialogTitle className="sr-only">
{previewIndex !== null
? `${app.displayName} screenshot ${previewIndex + 1} of ${appFrontend.screenshots.length}`
: 'Screenshot preview'}
</DialogTitle>
<div className="relative w-full h-full flex items-center justify-center p-4">
{previewIndex !== null && (
<>
{/* Close button */}
<button
onClick={() => navigatePreview('prev')}
className="absolute left-4 top-1/2 -translate-y-1/2 z-50 bg-white/10 hover:bg-white/20 p-3 rounded-full transition-colors"
aria-label="Previous screenshot"
onClick={() => setPreviewIndex(null)}
className="absolute top-4 right-4 z-50 bg-white/10 hover:bg-white/20 p-2 rounded-full transition-colors"
aria-label="Close preview"
>
<ChevronLeft className="w-8 h-8 text-white" />
<X className="w-6 h-6 text-white" />
</button>
)}
{/* Next button */}
{previewIndex < appFrontend.screenshots.length - 1 && (
<button
onClick={() => navigatePreview('next')}
className="absolute right-4 top-1/2 -translate-y-1/2 z-50 bg-white/10 hover:bg-white/20 p-3 rounded-full transition-colors"
aria-label="Next screenshot"
>
<ChevronRight className="w-8 h-8 text-white" />
</button>
)}
{/* Image counter */}
<div className="absolute top-4 left-4 z-50 bg-white/10 px-3 py-1 rounded-full text-white text-sm">
{previewIndex + 1} / {appFrontend.screenshots.length}
</div>
{/* Image */}
<div className="relative w-full h-[85vh] flex items-center justify-center">
<Image
src={appFrontend.screenshots[previewIndex]}
alt={`${app.displayName} screenshot ${previewIndex + 1}`}
fill
className="object-contain"
sizes="100vw"
/>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
{/* Previous button */}
{previewIndex > 0 && (
<button
onClick={() => navigatePreview('prev')}
className="absolute left-4 top-1/2 -translate-y-1/2 z-50 bg-white/10 hover:bg-white/20 p-3 rounded-full transition-colors"
aria-label="Previous screenshot"
>
<ChevronLeft className="w-8 h-8 text-white" />
</button>
)}
{/* Next button */}
{previewIndex < appFrontend.screenshots.length - 1 && (
<button
onClick={() => navigatePreview('next')}
className="absolute right-4 top-1/2 -translate-y-1/2 z-50 bg-white/10 hover:bg-white/20 p-3 rounded-full transition-colors"
aria-label="Next screenshot"
>
<ChevronRight className="w-8 h-8 text-white" />
</button>
)}
{/* Image */}
<div className="relative w-full h-[85vh] flex items-center justify-center">
<Image
src={appFrontend.screenshots[previewIndex]}
alt={`${app.displayName} screenshot ${previewIndex + 1}`}
fill
className="object-contain"
sizes="100vw"
/>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
</div>
</ScrollArea>
);