Overview
import { useState } from "react";
import { type ActivityComponentType } from "@stackflow/react/future";
import { AppBar, AppBarMain } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { TabsRoot, TabsTrigger, TabsList } from "seed-design/ui/tabs";
import { ErrorState } from "seed-design/ui/error-state";
import { SnackbarProvider } from "seed-design/ui/snackbar";
import { Recommendations } from "@/examples/react/demo/tabs/recommendations";
declare module "@stackflow/config" {
interface Register {
"react/demo/index": unknown;
}
}
const TABS = [
{ label: "추천", value: "recommendations" },
{ label: "구독", value: "subscriptions" },
] as const satisfies {
label: string;
value: string;
}[];
type Tab = (typeof TABS)[number]["value"];
const DemoActivity: ActivityComponentType<"react/demo/index"> = () => {
const [tab, setTab] = useState<Tab>("recommendations");
return (
<SnackbarProvider>
<style
// biome-ignore lint/security/noDangerouslySetInnerHtml: this is for hiding scrollbar
dangerouslySetInnerHTML={{
__html: "::-webkit-scrollbar{display:none}",
}}
/>
<AppScreen>
<AppBar tone="layer">
<AppBarMain title="Demo" />
</AppBar>
<AppScreenContent>
<TabsRoot
value={tab}
onValueChange={(value) => setTab(value as Tab)}
triggerLayout="fill"
size="medium"
stickyList
style={{ height: "100%", overflowY: "auto" }}
>
<TabsList>
{TABS.map(({ label, value }) => (
<TabsTrigger key={value} value={value}>
{label}
</TabsTrigger>
))}
</TabsList>
{tab === "recommendations" && <Recommendations />}
{tab === "subscriptions" && (
<ErrorState
title="구독한 글이 없습니다."
description="추천 글을 확인해보세요."
primaryActionProps={{
children: "추천 글 보기",
onClick: () => setTab("recommendations"),
}}
/>
)}
</TabsRoot>
</AppScreenContent>
</AppScreen>
</SnackbarProvider>
);
};
export default DemoActivity;import { CATEGORIES, type Article } from "@/examples/react/demo/data";
import type { ActivityComponentType } from "@stackflow/react/future";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarRight,
AppBarLeft,
} from "seed-design/ui/app-bar";
import { VStack, HStack, Box } from "@seed-design/react";
import { Text } from "@seed-design/react";
import { Badge } from "@seed-design/react";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
import { Callout } from "seed-design/ui/callout";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";
import { ErrorState } from "seed-design/ui/error-state";
import { ActionButton } from "seed-design/ui/action-button";
import { Skeleton } from "@seed-design/react";
import { IconILowercaseSerifCircleFill } from "@karrotmarket/react-monochrome-icon";
import { ArticleAuthor } from "./components/article-author";
import { formatDate } from "@/examples/react/demo/utils/date";
import { useState } from "react";
import img from "@/public/penguin.webp";
declare module "@stackflow/config" {
interface Register {
"react/demo/article-detail": {
article: Article;
};
}
}
const SEGMENTS = [
{ value: "popular", label: "인기" },
{ value: "latest", label: "최근" },
] as const satisfies { value: string; label: string }[];
const DemoArticleDetail: ActivityComponentType<"react/demo/article-detail"> = ({
params: { article },
}: {
// XXX: this should be inferred
params: { article: Article };
}) => {
const categoryName = CATEGORIES.find((c) => c.id === article.categoryId)?.name;
const [isImageLoading, setIsImageLoading] = useState(true);
return (
<AppScreen layerOffsetTop="none">
<AppBar tone="transparent">
<AppBarLeft>
<AppBarBackButton />
</AppBarLeft>
<AppBarRight>
<AppBarCloseButton />
</AppBarRight>
</AppBar>
<AppScreenContent>
<VStack gap="x4">
<Box style={{ aspectRatio: "1 / 1", position: "relative" }}>
<img
src={img.src}
alt="penguin"
onLoad={() => setIsImageLoading(false)}
style={{ position: "absolute", zIndex: 1 }}
/>
{isImageLoading && <Skeleton width="full" height="full" radius="0" />}
</Box>
<VStack gap="x6" pb="x4">
<VStack px="spacingX.globalGutter" gap="spacingY.componentDefault" align="flex-start">
{article.isPopular && (
<Badge variant="outline" tone="brand" size="large">
인기
</Badge>
)}
<VStack gap="x1">
<Text as="h1" textStyle="t7Bold" color="fg.neutral">
{article.title}
</Text>
<Text
as="p"
textStyle="articleBody"
color="fg.neutralMuted"
style={{ wordBreak: "keep-all" }}
>
{article.content}
</Text>
</VStack>
<HStack width="full" align="center">
<ArticleAuthor author={article.author} />
<Text textStyle="t2Regular" color="fg.neutralMuted">
{categoryName} ⸱ {formatDate(article.createdAt)}
</Text>
</HStack>
</VStack>
<VStack px="spacingX.globalGutter" gap="spacingY.componentDefault">
<Callout
tone="neutral"
description="따뜻한 댓글을 남겨주세요."
prefixIcon={<IconILowercaseSerifCircleFill />}
/>
<SegmentedControl
aria-label="댓글 정렬 방식"
defaultValue={SEGMENTS[0].value}
style={{ width: "100%" }}
>
{SEGMENTS.map((tab) => (
<SegmentedControlItem key={tab.value} value={tab.value}>
{tab.label}
</SegmentedControlItem>
))}
</SegmentedControl>
<Box py="x3">
<ErrorState title="댓글 없음" description="댓글이 없습니다." />
</Box>
<TextField label="댓글" maxGraphemeCount={200}>
<TextFieldTextarea placeholder="저는…" />
</TextField>
<ActionButton>게시</ActionButton>
</VStack>
</VStack>
</VStack>
</AppScreenContent>
</AppScreen>
);
};
export default DemoArticleDetail;Last updated on