Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/[lang]/products/app-store/[slug]/components/AppDetailHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ 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';
'hidden lg:flex absolute left-[609px] top-9 z-20 h-[130px] w-[130px] -translate-x-1/2 items-center justify-center rounded-[5.235px] border-[0.5px] border-transparent p-5 shadow-2xl shadow-black/40';

const heroCenterLogoBorderStyle = {
background:
'linear-gradient(#0A0A0A, #0A0A0A) padding-box, linear-gradient(109.08deg, #FFFFFF 0.55%, rgba(255, 255, 255, 0) 26.65%) border-box, linear-gradient(285.16deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 8.87%) border-box, linear-gradient(0deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.15)) border-box',
};

function textLinkClassName(variant: 'primary' | 'default' = 'default') {
return cn(
Expand Down Expand Up @@ -102,7 +107,10 @@ export default function AppDetailHero({ app, lang }: AppDetailHeroProps) {
</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}>
<div
className={heroCenterLogoClassName}
style={heroCenterLogoBorderStyle}
>
<AppIcon
src={app.icon}
alt={`${app.name} icon`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
'use client';

import { useEffect, useMemo, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { ExternalLink, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { AppDetailConfig } from './app-detail-utils';

Expand All @@ -20,10 +16,10 @@ type ReadmeCandidate = {
blobBaseUrl: string;
};

type ReadmeState =
| { status: 'idle' | 'loading' }
| { status: 'ready'; markdown: string; source: ReadmeCandidate }
| { status: 'error' };
export type LoadedReadmeMarkdown = {
markdown: string;
source: ReadmeCandidate;
};

const README_FILENAMES = ['README.md', 'README.MD', 'Readme.md', 'readme.md'];
const README_DIRECTORIES = ['', 'docs'];
Expand Down Expand Up @@ -132,6 +128,58 @@ function buildReadmeCandidates({
);
}

function buildDirectReadmeSource(readmeUrl: string): ReadmeSource | null {
if (isUnsafeUrl(readmeUrl)) return null;

try {
const url = new URL(readmeUrl);
const pathParts = url.pathname.split('/').filter(Boolean);
const rawAssetBaseUrl = new URL('./', url).toString();

let repoLabel = url.hostname;
let repoUrl = readmeUrl;
let blobBaseUrl = rawAssetBaseUrl;

if (
url.hostname.toLowerCase() === 'raw.githubusercontent.com' &&
pathParts.length >= 4
) {
const [owner, repo, branch, ...repoPathParts] = pathParts;
const encodedOwner = encodeURIComponent(owner);
const encodedRepo = encodeURIComponent(repo);
const encodedBranch = encodeURIComponent(branch);
const encodedDirectoryPath = encodePathSegments(repoPathParts.slice(0, -1));

repoLabel = `${owner}/${repo}`;
repoUrl = `https://github.com/${encodedOwner}/${encodedRepo}`;
blobBaseUrl = [
repoUrl,
'blob',
encodedBranch,
encodedDirectoryPath,
]
.filter(Boolean)
.join('/');
blobBaseUrl = `${blobBaseUrl}/`;
}

return {
repoLabel,
repoUrl,
candidates: [
{
sourcePath: 'README.md',
url: readmeUrl,
rawAssetBaseUrl,
blobBaseUrl,
},
],
};
} catch {
return null;
}
}

export function normalizeGitHubMarkdown(markdown: string) {
return markdown
.replace(/<br\s*\/?>/gi, '\n')
Expand All @@ -150,8 +198,14 @@ export function normalizeGitHubMarkdown(markdown: string) {
}

export function getReadmeSource(
app: Pick<AppDetailConfig, 'github' | 'website'>,
app: Pick<AppDetailConfig, 'readme' | 'github' | 'website'>,
): ReadmeSource | null {
const readmeUrl = app.readme || undefined;
if (readmeUrl) {
const directSource = buildDirectReadmeSource(readmeUrl);
if (directSource) return directSource;
}
Comment on lines +203 to +207

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back to GitHub source when configured README fails

This change makes app.readme authoritative: if the URL parses, getReadmeSource returns it immediately and loadReadmeMarkdown later converts any fetch failure into null, so the statically generated page permanently renders the screenshot fallback for that app build. In the app-store detail flow (page.tsx + loadReadmeMarkdown), a typo/404/transient outage on the configured README URL now suppresses the previously available GitHub/website candidate resolution path instead of falling back to it.

Useful? React with 👍 / 👎.


const githubUrl = isGithubUrl(app.github)
? app.github
: isGithubUrl(app.website)
Expand Down Expand Up @@ -233,10 +287,10 @@ function resolveMarkdownUrl(
}
}

async function fetchReadme(source: ReadmeSource, signal: AbortSignal) {
async function fetchReadme(source: ReadmeSource) {
for (const candidate of source.candidates) {
const response = await fetch(candidate.url, {
signal,
cache: 'force-cache',
headers: {
Accept: 'text/plain,text/markdown,*/*',
},
Expand All @@ -256,6 +310,24 @@ async function fetchReadme(source: ReadmeSource, signal: AbortSignal) {
throw new Error('README not found.');
}

export async function loadReadmeMarkdown(
app: Pick<AppDetailConfig, 'readme' | 'github' | 'website'>,
): Promise<LoadedReadmeMarkdown | null> {
const readmeSource = getReadmeSource({
readme: app.readme,
github: app.github,
website: app.website,
});

if (!readmeSource) return null;

try {
return await fetchReadme(readmeSource);
} catch {
return null;
}
}

function ReadmeFallback({ app }: { app: AppDetailConfig }) {
const screenshot = app.screenshots?.[0];

Expand All @@ -280,85 +352,23 @@ function ReadmeFallback({ app }: { app: AppDetailConfig }) {
);
}

function ReadmeLoading() {
return (
<div className="flex aspect-video min-h-[260px] items-center justify-center bg-[#0d1117] text-sm text-zinc-400">
<span className="inline-flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading README from GitHub...
</span>
</div>
);
}

export default function ReadmeMarkdownWindow({
app,
readme,
}: {
app: AppDetailConfig;
readme: LoadedReadmeMarkdown | null;
}) {
const readmeSource = useMemo(
() => getReadmeSource({ github: app.github, website: app.website }),
[app.github, app.website],
);
const [readme, setReadme] = useState<ReadmeState>({ status: 'idle' });

useEffect(() => {
if (!readmeSource) {
setReadme({ status: 'error' });
return;
}

const controller = new AbortController();
setReadme({ status: 'loading' });

fetchReadme(readmeSource, controller.signal)
.then(({ markdown, source }) => {
setReadme({ status: 'ready', markdown, source });
})
.catch(() => {
if (controller.signal.aborted) return;

setReadme({ status: 'error' });
});

return () => controller.abort();
}, [readmeSource]);

if (!readmeSource || readme.status === 'error') {
if (!readme) {
return <ReadmeFallback app={app} />;
}

if (readme.status !== 'ready') {
return <ReadmeLoading />;
}

return (
<div
aria-label={`${app.name} README content`}
className="aspect-video min-h-[260px] overflow-y-auto overscroll-contain bg-[#0d1117] px-5 py-6 text-sm text-zinc-300 [scrollbar-color:#335f91_#0d1117] [scrollbar-width:thin] sm:px-8"
role="region"
>
<div className="mb-5 flex flex-wrap items-center justify-between gap-3 border-b border-white/10 pb-4 text-xs text-zinc-500">
<a
href={readmeSource.repoUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-zinc-300 transition hover:text-[#69a3ff]"
>
{readmeSource.repoLabel}
<ExternalLink className="h-3 w-3" />
</a>
<a
href={readme.source.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 transition hover:text-[#69a3ff]"
>
Rendered from {readme.source.sourcePath}
<ExternalLink className="h-3 w-3" />
</a>
</div>

<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { figmaDetailHeadingClassName } from './SectionHeading';
import type { AppDetailConfig } from './app-detail-utils';
import ReadmeMarkdownWindow from './ReadmeMarkdownWindow';
import ReadmeMarkdownWindow, {
type LoadedReadmeMarkdown,
} from './ReadmeMarkdownWindow';

interface ReadmePreviewProps {
app: AppDetailConfig;
readme: LoadedReadmeMarkdown | null;
}

export default function ReadmePreview({ app }: ReadmePreviewProps) {
export default function ReadmePreview({ app, readme }: ReadmePreviewProps) {
return (
<section
id="readme"
Expand Down Expand Up @@ -35,7 +38,7 @@ export default function ReadmePreview({ app }: ReadmePreviewProps) {
<span className="w-[42px]" aria-hidden="true" />
</div>

<ReadmeMarkdownWindow app={app} />
<ReadmeMarkdownWindow app={app} readme={readme} />
</div>
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from 'lucide-react';
import SealosLogo from '@/assets/shared-icons/sealos.svg';
import K8sLogo from '@/app/[lang]/(home)/(new-home)/components/carousel-image/DeploymentCard/logo/k8s.svg';
import { cn } from '@/lib/utils';
import SectionHeading from './SectionHeading';

type DeployStep =
Expand Down Expand Up @@ -71,6 +72,20 @@ const resourceIcons = [
{ label: 'Observability', icon: Activity },
];

const gradientBorderStyle = {
background:
'linear-gradient(#0A0A0A, #0A0A0A) padding-box, linear-gradient(109.08deg, #FFFFFF 0.55%, rgba(255, 255, 255, 0) 26.65%) border-box, linear-gradient(285.16deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 8.87%) border-box, linear-gradient(0deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.15)) border-box',
};

const diagramCardClassName =
'border-[0.5px] border-transparent bg-[#0A0A0A]';

const diagramTopCardClassName = `${diagramCardClassName} rounded-xl p-3`;

const diagramResourceGridClassName = `${diagramCardClassName} grid w-full grid-cols-2 overflow-hidden rounded-lg sm:grid-cols-5`;

const diagramLiveCardClassName = `${diagramCardClassName} inline-flex items-center gap-3 rounded-lg px-5 py-4 text-sm font-semibold text-white`;

export default function WhyDeployOnSealos() {
return (
<section className="mx-auto max-w-7xl px-6 pt-14 pb-16 lg:px-8 lg:pt-16 lg:pb-24">
Expand All @@ -82,8 +97,11 @@ export default function WhyDeployOnSealos() {
<div className="mt-16 grid gap-12 lg:grid-cols-[minmax(0,520px)_minmax(0,1fr)] lg:items-center">
<div className="relative min-h-[460px] overflow-hidden p-8">
<div className="relative mx-auto flex max-w-[430px] flex-col items-center">
<div className="rounded-xl border border-white/10 bg-white/[0.045] p-3">
<div className="flex h-10 items-center gap-3 rounded-lg bg-white/[0.055] px-4 text-sm font-semibold text-white">
<div
className={diagramTopCardClassName}
style={gradientBorderStyle}
>
<div className="flex h-10 items-center gap-3 px-4 text-sm font-semibold text-white">
<Grid2X2 className="h-4 w-4 text-[#69a3ff]" />
One-Click Deploy
</div>
Expand All @@ -99,11 +117,17 @@ export default function WhyDeployOnSealos() {
/>
</div>
<div className="h-12 w-px bg-white/10" />
<div className="grid w-full grid-cols-2 gap-3 sm:grid-cols-5">
{resourceIcons.map((item) => (
<div
className={diagramResourceGridClassName}
style={gradientBorderStyle}
>
{resourceIcons.map((item, index) => (
<div
key={item.label}
className="flex min-h-[74px] flex-col items-center justify-center gap-2 rounded-lg border border-white/10 bg-white/[0.035] text-center"
className={cn(
'flex min-h-[74px] flex-col items-center justify-center gap-2 text-center',
index > 0 ? 'border-l border-white/10' : '',
)}
>
<item.icon className="h-5 w-5 text-zinc-300" />
<span className="text-[11px] text-zinc-400">
Expand All @@ -113,7 +137,10 @@ export default function WhyDeployOnSealos() {
))}
</div>
<div className="h-12 w-px bg-white/10" />
<div className="inline-flex items-center gap-3 rounded-lg border border-white/10 bg-white/[0.04] px-5 py-4 text-sm font-semibold text-white">
<div
className={diagramLiveCardClassName}
style={gradientBorderStyle}
>
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white/[0.055]">
<CircleCheckBig className="h-6 w-6 text-[#22C55E]" />
</span>
Expand Down
Loading