diff --git a/src/components/Header.astro b/src/components/Header.astro index 57844d5..3cf58ba 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -27,7 +27,7 @@ const homeUrl = lang === "ja" ? "/ja/" : "/";
diff --git a/src/components/community-feed/CommunityFeedFilters.tsx b/src/components/community-feed/CommunityFeedFilters.tsx new file mode 100644 index 0000000..387a376 --- /dev/null +++ b/src/components/community-feed/CommunityFeedFilters.tsx @@ -0,0 +1,285 @@ +import { useState, useEffect, useCallback } from "react"; +import type { TagId } from "../../types/community-feed"; +import { TAG_LABELS, ALLOWED_TAGS } from "../../types/community-feed"; + +type Translations = { + filterAll: string; + filterBlog: string; + filterVideo: string; + filterProject: string; + allMembers: string; + allTime: string; + lastWeek: string; + lastMonth: string; + last3Months: string; + clearAll: string; + emptyState: string; + emptyCta: string; + filterType: string; + filterMember: string; + filterTags: string; + filterTime: string; +}; + +type MemberOption = { + id: string; + name: string; +}; + +type Props = { + members: MemberOption[]; + lang: "en" | "ja"; + translations: Translations; +}; + +type TimeFilter = "all" | "week" | "month" | "3months"; + +export default function CommunityFeedFilters({ + members, + lang, + translations: t, +}: Props) { + const [typeFilter, setTypeFilter] = useState("all"); + const [memberFilter, setMemberFilter] = useState("all"); + const [tagFilters, setTagFilters] = useState>(new Set()); + const [timeFilter, setTimeFilter] = useState("all"); + const [memberDropdownOpen, setMemberDropdownOpen] = useState(false); + const [timeDropdownOpen, setTimeDropdownOpen] = useState(false); + + const isDefault = + typeFilter === "all" && + memberFilter === "all" && + tagFilters.size === 0 && + timeFilter === "all"; + + const clearAll = () => { + setTypeFilter("all"); + setMemberFilter("all"); + setTagFilters(new Set()); + setTimeFilter("all"); + }; + + const toggleTag = (tag: TagId) => { + setTagFilters((prev) => { + const next = new Set(prev); + if (next.has(tag)) next.delete(tag); + else next.add(tag); + return next; + }); + }; + + const getTimeFilterDate = useCallback((filter: TimeFilter): string | null => { + if (filter === "all") return null; + const now = new Date(); + if (filter === "week") now.setDate(now.getDate() - 7); + else if (filter === "month") now.setMonth(now.getMonth() - 1); + else if (filter === "3months") now.setMonth(now.getMonth() - 3); + return now.toISOString().split("T")[0]; + }, []); + + // Apply filters to card DOM elements + useEffect(() => { + const cards = document.querySelectorAll(".cf-card"); + const minDate = getTimeFilterDate(timeFilter); + let visibleCount = 0; + + cards.forEach((card) => { + const cardType = card.dataset.type ?? ""; + const cardMember = card.dataset.member ?? ""; + const cardTags = (card.dataset.tags ?? "").split(","); + const cardDate = card.dataset.date ?? ""; + + const passesType = typeFilter === "all" || cardType === typeFilter; + const passesMember = + memberFilter === "all" || cardMember === memberFilter; + const passesTags = + tagFilters.size === 0 || + cardTags.some((t) => tagFilters.has(t as TagId)); + const passesTime = !minDate || cardDate >= minDate; + + const visible = passesType && passesMember && passesTags && passesTime; + card.style.display = visible ? "" : "none"; + if (visible) visibleCount++; + }); + + // Toggle empty state + const emptyEl = document.getElementById("cf-empty-state"); + if (emptyEl) { + emptyEl.style.display = visibleCount === 0 ? "flex" : "none"; + } + }, [typeFilter, memberFilter, tagFilters, timeFilter, getTimeFilterDate]); + + // Close dropdowns on outside click + useEffect(() => { + const handler = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest(".cf-member-dropdown")) setMemberDropdownOpen(false); + if (!target.closest(".cf-time-dropdown")) setTimeDropdownOpen(false); + }; + document.addEventListener("click", handler); + return () => document.removeEventListener("click", handler); + }, []); + + const typePills = [ + { value: "all", label: t.filterAll }, + { value: "blog", label: `\u{1F4DD} ${t.filterBlog}` }, + { value: "video", label: `\u{1F3AC} ${t.filterVideo}` }, + { value: "project", label: `\u{1F680} ${t.filterProject}` }, + ]; + + const timeOptions = [ + { value: "all" as const, label: t.allTime }, + { value: "week" as const, label: t.lastWeek }, + { value: "month" as const, label: t.lastMonth }, + { value: "3months" as const, label: t.last3Months }, + ]; + + const selectedMemberLabel = + memberFilter === "all" + ? t.allMembers + : members.find((m) => m.id === memberFilter)?.name ?? t.allMembers; + + const selectedTimeLabel = + timeOptions.find((o) => o.value === timeFilter)?.label ?? t.allTime; + + const pillBase = + "rounded-full border px-3 py-1.5 text-[13px] font-medium transition-all duration-150 cursor-pointer"; + const pillInactive = `${pillBase} border-[#ddd] text-[#888] hover:border-[#bbb] hover:text-[#666]`; + const pillActive = `${pillBase} border-[#1a1a1a] bg-[#1a1a1a] text-white`; + + return ( +
+
+ {/* Type filter */} +
+ + {t.filterType} + +
+ {typePills.map((pill) => ( + + ))} +
+
+ + {/* Divider */} +
+ + {/* Member dropdown */} +
+ + {t.filterMember} + + + {memberDropdownOpen && ( +
+ + {members.map((m) => ( + + ))} +
+ )} +
+ + {/* Divider */} +
+ + {/* Tag pills */} +
+ + {t.filterTags} + +
+ {ALLOWED_TAGS.map((tag) => ( + + ))} +
+
+ + {/* Divider */} +
+ + {/* Time dropdown */} +
+ + {t.filterTime} + + + {timeDropdownOpen && ( +
+ {timeOptions.map((opt) => ( + + ))} +
+ )} +
+ + {/* Clear all */} + {!isDefault && ( + + )} +
+
+ ); +} diff --git a/src/components/community-feed/ContentCard.astro b/src/components/community-feed/ContentCard.astro new file mode 100644 index 0000000..611b0b2 --- /dev/null +++ b/src/components/community-feed/ContentCard.astro @@ -0,0 +1,163 @@ +--- +import { getMemberById } from "../../utils/community-feed"; +import { TAG_LABELS, type TagId } from "../../types/community-feed"; +import type { Post } from "../../types/community-feed"; +import { useTranslations } from "../../i18n/utils"; + +type Props = { + post: Post; + lang: "en" | "ja"; +}; + +const { post, lang } = Astro.props; +const t = useTranslations(lang); +const member = getMemberById(post.member); +const firstTag = post.tags[0] as TagId; +const tagLabel = TAG_LABELS[firstTag]?.[lang] ?? firstTag; + +const FALLBACK_GRADIENTS: Record = { + blog: "linear-gradient(135deg, #f5f0eb, #e8e0d8)", + video: "linear-gradient(135deg, #2a2a35, #1a1a25)", + project: "linear-gradient(135deg, #eaf2ed, #d8e8df)", +}; +const FALLBACK_EMOJI: Record = { + blog: "\u{1F4DD}", + video: "\u{1F3AC}", + project: "\u{1F680}", +}; + +const hasThumbnail = !!post.thumbnail; +const fallbackGradient = FALLBACK_GRADIENTS[post.type]; +const fallbackEmoji = FALLBACK_EMOJI[post.type]; + +const DATE_LOCALES: Record<"en" | "ja", string> = { + en: "en-US", + ja: "ja-JP", +}; +const formattedDate = new Intl.DateTimeFormat(DATE_LOCALES[lang], { + year: "numeric", + month: "long", + day: "numeric", +}).format(new Date(post.date)); +--- + +
+ + +
+ {hasThumbnail ? ( + + ) : ( +
+ {fallbackEmoji} +
+ )} + {/* Dark gradient overlay for text readability */} +
+
+

+ {post.title} +

+ + {tagLabel} + +
+
+
+ + +
+ {post.description && ( +

+ {post.description} +

+ )} + + + +
+ + 📅 + + {t("communityFeed.updatedAt")}: {formattedDate} +
+ + {post.type === "project" && ( +
+ + {post.techStack && post.techStack.length > 0 && ( +
+ {post.techStack.map((tech: string) => ( + + {tech} + + ))} +
+ )} +
+ )} + + {/* Extra excerpt line for non-project cards */} + {post.type !== "project" && post.description && ( +

+ {post.description} +

+ )} +
+
diff --git a/src/components/community-feed/FeaturedPost.astro b/src/components/community-feed/FeaturedPost.astro new file mode 100644 index 0000000..4e15334 --- /dev/null +++ b/src/components/community-feed/FeaturedPost.astro @@ -0,0 +1,120 @@ +--- +import { useTranslations } from "../../i18n/utils"; +import { getMemberById } from "../../utils/community-feed"; +import { TAG_LABELS, type TagId } from "../../types/community-feed"; +import type { Post } from "../../types/community-feed"; + +type Props = { + post: Post; + lang: "en" | "ja"; +}; + +const { post, lang } = Astro.props; +const t = useTranslations(lang); +const member = getMemberById(post.member); +const firstTag = post.tags[0] as TagId; +const tagLabel = TAG_LABELS[firstTag]?.[lang] ?? firstTag; + +const dateFormatted = new Date(post.date).toLocaleDateString( + lang === "ja" ? "ja-JP" : "en-US", + { year: "numeric", month: "long", day: "numeric" } +); + +const FALLBACK_GRADIENTS: Record = { + blog: "linear-gradient(135deg, #f5f0eb, #e8e0d8)", + video: "linear-gradient(135deg, #2a2a35, #1a1a25)", + project: "linear-gradient(135deg, #eaf2ed, #d8e8df)", +}; +const FALLBACK_EMOJI: Record = { + blog: "\u{1F4DD}", + video: "\u{1F3AC}", + project: "\u{1F680}", +}; + +const hasThumbnail = !!post.thumbnail; +const fallbackGradient = FALLBACK_GRADIENTS[post.type]; +const fallbackEmoji = FALLBACK_EMOJI[post.type]; +--- + + diff --git a/src/components/community-feed/PageHeader.astro b/src/components/community-feed/PageHeader.astro new file mode 100644 index 0000000..0a98c69 --- /dev/null +++ b/src/components/community-feed/PageHeader.astro @@ -0,0 +1,22 @@ +--- +import { useTranslations } from "../../i18n/utils"; + +type Props = { + lang: "en" | "ja"; +}; + +const { lang } = Astro.props; +const t = useTranslations(lang); +--- + +
+

+ {t("communityFeed.eyebrow")} +

+

+ {t("communityFeed.title")} +

+

+ {t("communityFeed.subtitle")} +

+
diff --git a/src/data/community-feed.json b/src/data/community-feed.json new file mode 100644 index 0000000..b067cee --- /dev/null +++ b/src/data/community-feed.json @@ -0,0 +1,91 @@ +{ + "members": [ + { + "id": "ashwin-anil", + "name": "Ashwin Anil", + "website": "https://ashwin.im" + }, + { + "id": "mere-mortal", + "name": "Mere Mortal", + "website": "https://meremortal.dev" + }, + { + "id": "sam-chen", + "name": "Sam Chen", + "website": "https://samchen.dev" + }, + { + "id": "yuki-tanaka", + "name": "Yuki Tanaka", + "website": "https://yukitanaka.io" + }, + { + "id": "hiroshi-saito", + "name": "Hiroshi Saito", + "website": "https://hiroshisaito.com" + } + ], + "posts": [ + { + "member": "ashwin-anil", + "title": "Approach for building MVPs", + "url": "https://ashwin.im/blog/approach-for-building-mvps/", + "type": "blog", + "date": "2026-03-17", + "description": "Notes on building an MVP extremely quickly without losing the momentum. Covers the mindset, tooling choices, and iteration patterns that work best for solo founders.", + "thumbnail": "https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=1200&q=80", + "tags": ["startups"] + }, + { + "member": "mere-mortal", + "title": "Putting OpenClaw on a Raspberry Pi", + "url": "https://www.youtube.com/watch?v=UDEhlZGbza0", + "type": "video", + "date": "2026-03-01", + "description": "Getting the OpenClaw controller running on a Raspberry Pi with full GPIO integration.", + "thumbnail": "https://i.ytimg.com/vi/UDEhlZGbza0/hqdefault.jpg", + "tags": ["open-source", "creative-coding"] + }, + { + "member": "sam-chen", + "title": "KyotoMap", + "url": "https://kyotomap.app", + "type": "project", + "date": "2026-02-20", + "description": "Interactive map of tech-friendly cafes and coworking spaces in Kyoto. Built for the community, by the community.", + "thumbnail": "https://images.unsplash.com/photo-1493976040374-85c8e12f0c0e?w=600&q=80", + "tags": ["web-dev", "life-in-japan"], + "repoUrl": "https://github.com/samchen/kyotomap", + "techStack": ["React", "Mapbox", "Supabase"] + }, + { + "member": "yuki-tanaka", + "title": "Understanding the state of AI agents in 2026", + "url": "https://yukitanaka.io/blog/ai-agents-2026", + "type": "blog", + "date": "2026-02-10", + "description": "A deep dive into the current landscape of AI agents, their capabilities, limitations, and what builders should know before integrating them.", + "thumbnail": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=600&q=80", + "tags": ["ai"] + }, + { + "member": "hiroshi-saito", + "title": "Building accessible React components from scratch", + "url": "https://hiroshisaito.com/blog/accessible-react", + "type": "blog", + "date": "2026-01-28", + "description": "Practical patterns for building React components that work for everyone. Covers ARIA, keyboard navigation, and screen reader testing.", + "tags": ["web-dev", "open-source"] + }, + { + "member": "mere-mortal", + "title": "Creative coding with p5.js in Kyoto", + "url": "https://www.youtube.com/watch?v=example123", + "type": "video", + "date": "2026-01-15", + "description": "Generative art session from our Kyoto meetup, creating visualizations inspired by traditional Japanese patterns.", + "tags": ["creative-coding"] + } + ] +} diff --git a/src/data/composite-feed.json b/src/data/composite-feed.json index 18377a8..b6146f0 100644 --- a/src/data/composite-feed.json +++ b/src/data/composite-feed.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-02-28T09:11:45.481Z", + "generatedAt": "2026-03-28T06:21:04.601Z", "itemsPerFeed": 3, "feeds": [ { @@ -8,43 +8,43 @@ "siteUrl": "https://www.ashryan.io/", "items": [ { - "id": "69964e9ff37e170001ebf954", - "title": "Kyoto Tech Meetup links for February 19, 2026", - "link": "https://www.ashryan.io/kyoto-tech-meetup-links-for-february-19-2026/", - "publishedAt": "2026-02-19T01:09:43.000Z", + "id": "69bb304d91aea700019d55f0", + "title": "Kyoto Tech Meetup links for March 19, 2026", + "link": "https://www.ashryan.io/kyoto-tech-meetup-links-for-march-19-2026/", + "publishedAt": "2026-03-19T01:26:21.000Z", "source": { "name": "Ash Ryan Arnwine", "siteUrl": "https://www.ashryan.io/", "feedUrl": "https://www.ashryan.io/rss/" }, - "summary": "\"Humans are hard\" edition", - "image": "https://images.unsplash.com/photo-1504630083234-14187a9df0f5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDQzfHxjb2ZmZWV8ZW58MHx8fHwxNzcxNDYyNjE4fDA&ixlib=rb-4.1.0&q=80&w=2000" + "summary": "As a non-large language model, I have no idea how to summarize the sheer breadth of these links.", + "image": "https://images.unsplash.com/photo-1503481766315-7a586b20f66d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDcwfHxjb2ZmZWV8ZW58MHx8fHwxNzczODgzNTI4fDA&ixlib=rb-4.1.0&q=80&w=2000" }, { - "id": "698fc74489aedc0001210e2d", - "title": "Kyoto Tech Meetup links for Feb 14, 2026", - "link": "https://www.ashryan.io/kyoto-tech-meetup-links-for-feb-14-2026/", - "publishedAt": "2026-02-14T02:27:09.000Z", + "id": "69b4b40c68dbec00018c8e81", + "title": "Kyoto Tech Meetup links for March 14, 20206", + "link": "https://www.ashryan.io/kyoto-tech-meetup-links-for-march-14-20206/", + "publishedAt": "2026-03-14T01:43:48.000Z", "source": { "name": "Ash Ryan Arnwine", "siteUrl": "https://www.ashryan.io/", "feedUrl": "https://www.ashryan.io/rss/" }, - "summary": "It's AI all the way down.", - "image": "https://images.unsplash.com/photo-1541167760496-1628856ab772?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fGNvZmZlZXxlbnwwfHx8fDE3NzEwMTI3NTl8MA&ixlib=rb-4.1.0&q=80&w=2000" + "summary": "MCPs? Skills? Tools? Raw LLM? What's today's AI weather?", + "image": "https://images.unsplash.com/photo-1497935586351-b67a49e012bf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI1fHxjb2ZmZWV8ZW58MHx8fHwxNzczNDUyNDgwfDA&ixlib=rb-4.1.0&q=80&w=2000" }, { - "id": "6980152b82e86e0001227784", - "title": "Kyoto Tech Meetup: January 2026 wrap-up", - "link": "https://www.ashryan.io/kyoto-tech-meetup-january-2026-wrap-up/", - "publishedAt": "2026-02-03T00:12:50.000Z", + "id": "69a8c9851f47ac000159a1d2", + "title": "Kyoto Tech Meetup links for March 5, 2026", + "link": "https://www.ashryan.io/kyoto-tech-meetup-links-for-march-5-2026/", + "publishedAt": "2026-03-05T02:05:48.000Z", "source": { "name": "Ash Ryan Arnwine", "siteUrl": "https://www.ashryan.io/", "feedUrl": "https://www.ashryan.io/rss/" }, - "summary": "Updates from our morning coffee and hack day events last month. Also, this month's schedule.", - "image": "https://www.ashryan.io/content/images/2026/02/IMG_5665--1--1.jpeg" + "summary": "Time management, OpenClaw, cherry blossoms, and more", + "image": "https://images.unsplash.com/photo-1506619216599-9d16d0903dfd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fGNvZmZlZXxlbnwwfHx8fDE3NzI2NzYzMjh8MA&ixlib=rb-4.1.0&q=80&w=2000" } ] }, @@ -98,48 +98,14 @@ "name": "Mere Mortal Dev", "feedUrl": "https://www.youtube.com/@meremortaldev", "siteUrl": "https://www.youtube.com/@meremortaldev", - "items": [ - { - "id": "yt:video:qWBRjtwUhKQ", - "title": "Git Worktree environments for AI agents in the OpenAI Codex app", - "link": "https://www.youtube.com/watch?v=qWBRjtwUhKQ", - "publishedAt": "2026-02-20T10:28:12.000Z", - "source": { - "name": "Mere Mortal Dev", - "siteUrl": "https://www.youtube.com/@meremortaldev", - "feedUrl": "https://www.youtube.com/@meremortaldev" - }, - "summary": "", - "image": "https://i.ytimg.com/vi/qWBRjtwUhKQ/hqdefault.jpg" - }, - { - "id": "yt:video:KHkGzQ2YGE0", - "title": "Prepping for Codex Git Worktrees video", - "link": "https://www.youtube.com/watch?v=KHkGzQ2YGE0", - "publishedAt": "2026-02-12T13:57:29.000Z", - "source": { - "name": "Mere Mortal Dev", - "siteUrl": "https://www.youtube.com/@meremortaldev", - "feedUrl": "https://www.youtube.com/@meremortaldev" - }, - "summary": "", - "image": "https://i.ytimg.com/vi/KHkGzQ2YGE0/hqdefault.jpg" - }, - { - "id": "yt:video:j106xpGv0AI", - "title": "OpenAI Codex app and git worktrees", - "link": "https://www.youtube.com/watch?v=j106xpGv0AI", - "publishedAt": "2026-02-12T03:42:59.000Z", - "source": { - "name": "Mere Mortal Dev", - "siteUrl": "https://www.youtube.com/@meremortaldev", - "feedUrl": "https://www.youtube.com/@meremortaldev" - }, - "summary": "", - "image": "https://i.ytimg.com/vi/j106xpGv0AI/hqdefault.jpg" - } - ] + "items": [], + "error": "HTTP 404 Not Found" } ], - "failedSources": [] + "failedSources": [ + { + "source": "Mere Mortal Dev", + "error": "HTTP 404 Not Found" + } + ] } \ No newline at end of file diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 30d5f3e..8d1ed52 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -141,6 +141,33 @@ export const ui = { "home.footer.meetup": "Meetup", "home.footer.contact": "Contact", + "communityFeed.eyebrow": "OUR PERSPECTIVES", + "communityFeed.title": "From the Community", + "communityFeed.subtitle": + "Latest posts, projects, and videos from Kyoto Tech members", + "communityFeed.new": "NEW", + "communityFeed.filterAll": "All", + "communityFeed.filterBlog": "Blog", + "communityFeed.filterVideo": "Video", + "communityFeed.filterProject": "Project", + "communityFeed.allMembers": "All Members", + "communityFeed.allTime": "All Time", + "communityFeed.lastWeek": "Last Week", + "communityFeed.lastMonth": "Last Month", + "communityFeed.last3Months": "Last 3 Months", + "communityFeed.clearAll": "Clear all", + "communityFeed.emptyState": "No posts match your filters.", + "communityFeed.emptyCta": "clearing all filters", + "communityFeed.liveDemo": "Live Demo", + "communityFeed.github": "GitHub", + "communityFeed.viewAll": "View all posts", + "communityFeed.filterType": "Type", + "communityFeed.filterMember": "Member", + "communityFeed.filterTags": "Tags", + "communityFeed.filterTime": "Time", + "communityFeed.author": "Author", + "communityFeed.updatedAt": "Updated", + "meta.siteName": "Kyoto Tech Meetup", "meta.title": "Kyoto Tech Meetup — Connect, learn, and build together in Japan's cultural heart", @@ -285,6 +312,33 @@ export const ui = { "home.footer.meetup": "Meetup", "home.footer.contact": "お問い合わせ", + "communityFeed.eyebrow": "メンバーの声", + "communityFeed.title": "コミュニティから", + "communityFeed.subtitle": + "メンバーが発信しているコンテンツ", + "communityFeed.new": "NEW", + "communityFeed.filterAll": "すべて", + "communityFeed.filterBlog": "ブログ", + "communityFeed.filterVideo": "動画", + "communityFeed.filterProject": "プロジェクト", + "communityFeed.allMembers": "すべてのメンバー", + "communityFeed.allTime": "全期間", + "communityFeed.lastWeek": "先週", + "communityFeed.lastMonth": "先月", + "communityFeed.last3Months": "過去3ヶ月", + "communityFeed.clearAll": "クリア", + "communityFeed.emptyState": "フィルターに一致する投稿がありません。", + "communityFeed.emptyCta": "フィルターをクリア", + "communityFeed.liveDemo": "デモ", + "communityFeed.github": "GitHub", + "communityFeed.viewAll": "すべての投稿を見る", + "communityFeed.filterType": "タイプ", + "communityFeed.filterMember": "メンバー", + "communityFeed.filterTags": "タグ", + "communityFeed.filterTime": "期間", + "communityFeed.author": "著者", + "communityFeed.updatedAt": "更新日", + "meta.siteName": "Kyoto Tech Meetup", "meta.title": "Kyoto Tech Meetup — 日本の文化都市・京都でつながり、学び、つくるコミュニティ", diff --git a/src/pages/community-feed.astro b/src/pages/community-feed.astro new file mode 100644 index 0000000..3952547 --- /dev/null +++ b/src/pages/community-feed.astro @@ -0,0 +1,118 @@ +--- +import Layout from "../layouts/Layout.astro"; +import Header from "../components/Header.astro"; +import Footer from "../components/Footer.astro"; +import PageHeader from "../components/community-feed/PageHeader.astro"; +import FeaturedPost from "../components/community-feed/FeaturedPost.astro"; +import ContentCard from "../components/community-feed/ContentCard.astro"; +import CommunityFeedFilters from "../components/community-feed/CommunityFeedFilters.tsx"; +import { useTranslations } from "../i18n/utils"; +import { + getFeaturedPost, + getGridPosts, + getMembersWithPosts, +} from "../utils/community-feed"; + +const { lang = "en" } = Astro.props as { lang?: "en" | "ja" }; +const t = useTranslations(lang); + +const featuredPost = getFeaturedPost(); +const gridPosts = getGridPosts(); +const members = getMembersWithPosts().map((m) => ({ id: m.id, name: m.name })); + +const filterTranslations = { + filterAll: t("communityFeed.filterAll"), + filterBlog: t("communityFeed.filterBlog"), + filterVideo: t("communityFeed.filterVideo"), + filterProject: t("communityFeed.filterProject"), + allMembers: t("communityFeed.allMembers"), + allTime: t("communityFeed.allTime"), + lastWeek: t("communityFeed.lastWeek"), + lastMonth: t("communityFeed.lastMonth"), + last3Months: t("communityFeed.last3Months"), + clearAll: t("communityFeed.clearAll"), + emptyState: t("communityFeed.emptyState"), + emptyCta: t("communityFeed.emptyCta"), + filterType: t("communityFeed.filterType"), + filterMember: t("communityFeed.filterMember"), + filterTags: t("communityFeed.filterTags"), + filterTime: t("communityFeed.filterTime"), +}; +--- + + +
+ +
+ + + + + + +
+ {gridPosts.map((post) => ( + + ))} +
+ + + +
+ +