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
5 changes: 4 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "bun-react-template",
Expand All @@ -10,7 +9,9 @@
"@tabler/icons-react": "3.34.1",
"@tanstack/react-query": "5.90.11",
"bun-plugin-tailwind": "0.0.15",
"clsx": "2.1.1",
"firebase": "11.8.1",
"jotai": "2.15.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-markdown": "10.1.0",
Expand Down Expand Up @@ -865,6 +866,8 @@

"join-path": ["join-path@1.1.1", "", { "dependencies": { "as-array": "^2.0.0", "url-join": "0.0.1", "valid-url": "^1" } }, "sha512-jnt9OC34sLXMLJ6YfPQ2ZEKrR9mB5ZbSnQb4LPaOx1c5rTzxpR33L18jjp0r75mGGTJmsil3qwN1B5IBeTnSSA=="],

"jotai": ["jotai@2.15.2", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-El86CCfXNMEOytp20NPfppqGGmcp6H6kIA+tJHdmASEUURJCYW4fh8nTHEnB8rUXEFAY1pm8PdHPwnrcPGwdEg=="],

"js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],

"jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"@tabler/icons-react": "3.34.1",
"@tanstack/react-query": "5.90.11",
"bun-plugin-tailwind": "0.0.15",
"clsx": "2.1.1",
"firebase": "11.8.1",
"jotai": "2.15.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-markdown": "10.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/assets/icons/apple-podcasts.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/assets/icons/spotify.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/atoms/podcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { atom } from "jotai";

export const isPlayingPodcastAtom = atom<boolean>(false);
13 changes: 12 additions & 1 deletion src/components/Layout/NavList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Accordion, Box, Divider, Text } from "@mantine/core";
import { IconVolume } from "@tabler/icons-react";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { useLocation } from "react-router";
import { isPlayingPodcastAtom } from "@/atoms/podcast";
import { useActivePostGroup } from "@/lib/postGroup";
import { postGroupCategories } from "@/lib/postGroupCategory";
import NavListFooter from "./NavListFooter";
Expand All @@ -12,6 +15,7 @@ export type NavListProps = {

export default function NavList({ onSelectPostGroup }: NavListProps) {
const { pathname } = useLocation();
const [isPlayingPodcast] = useAtom(isPlayingPodcastAtom);

const activePostGroup = useActivePostGroup();
const activePostGroupCategory = useMemo(() => {
Expand All @@ -26,7 +30,14 @@ export default function NavList({ onSelectPostGroup }: NavListProps) {
<NavListItem
id={null}
icon={null}
name="All"
name={
<>
<span className="flex-1">All</span>
{isPlayingPodcast && (
<IconVolume className="ml-1 size-4 text-[var(--mantine-color-gray-6)]" />
)}
</>
}
active={pathname === "/" && activePostGroup == null}
onSelectPostGroup={onSelectPostGroup}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Layout/NavListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Anchor } from "@mantine/core";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { clsx } from "@/lib/util";

type NavListItemProps = {
id: string | null;
icon: string | null;
name: string;
name: ReactNode;
active: boolean;

onSelectPostGroup: () => void;
Expand Down
40 changes: 39 additions & 1 deletion src/lib/post.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import {
collection,
type FirestoreDataConverter,
Expand All @@ -17,6 +17,11 @@ import {
type PostGroupName,
} from "./postGroup";

export type Cast = {
createdAt: Date;
mp3Url: string;
};

export type Post = {
id: string;
group: PostGroup;
Expand Down Expand Up @@ -103,3 +108,36 @@ export const usePosts = (group: PostGroupName | null) => {
posts: data ?? [],
};
};

export const usePodcast = () => {
const { data, ...values } = useQuery({
queryKey: ["podcast", "latest"],
gcTime: 60 * 30 * 1000,
queryFn: async () => {
return await _getCast();
},
});

return {
...values,
cast: data ?? null,
};
};

const _getCast = async (): Promise<Cast> => {
const castsCollection = collection(firestore, "casts");
const castsQuery = query(
castsCollection,
orderBy("createdAt", "desc"),
limit(1),
);
const snapshot = await getDocs(castsQuery);

const doc = snapshot.docs[0];
const data = doc.data();

return {
createdAt: data.createdAt.toDate(),
mp3Url: data.mp3Url,
};
};
88 changes: 86 additions & 2 deletions src/pages/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { Badge, Box, Button, Loader, Title } from "@mantine/core";
import {
Anchor,
Badge,
Box,
Button,
Card,
Loader,
Text,
Title,
} from "@mantine/core";
import { IconHeadphones } from "@tabler/icons-react";
import clsx from "clsx";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { usePosts } from "@/lib/post";
import ApplePodcastsIcon from "@/assets/icons/apple-podcasts.svg";
import SpotifyIcon from "@/assets/icons/spotify.svg";
import { isPlayingPodcastAtom } from "@/atoms/podcast";
import { usePodcast, usePosts } from "@/lib/post";
import { useActivePostGroup } from "@/lib/postGroup";
import { groupBy } from "@/lib/util";
import PostList from "./PostList";
Expand All @@ -17,9 +32,11 @@ const getDateText = (dateISOString: string) => {

export default function HomePage() {
const activePostGroup = useActivePostGroup();
const { cast } = usePodcast();
const { posts, isFetching, hasNextPage, fetchNextPage } = usePosts(
activePostGroup?.name ?? null,
);
const [, setIsPlayingPodcast] = useAtom(isPlayingPodcastAtom);

const postsGroupedByDate = useMemo(() => {
return groupBy(posts, (post) => {
Expand All @@ -35,6 +52,73 @@ export default function HomePage() {

return (
<Box className="flex flex-col p-4 pb-28">
<Box className={clsx("flex justify-center", activePostGroup && "hidden")}>
<Card
className="mb-4 flex w-full flex-col items-center gap-3 px-8 sm:w-3/4 md:w-4/5 lg:w-1/2"
withBorder
>
<Box className="flex flex-col items-center">
<Text
size="lg"
fw="bold"
classNames={{ root: "flex items-center" }}
>
<IconHeadphones className="mr-1 size-5" />
SAMARI Podcast
</Text>
<Text size="sm">最近要約された記事を音声で紹介</Text>
</Box>

{cast && (
<Box className="flex w-full flex-col items-end">
<Text c="gray" size="sm">
{cast.createdAt.toLocaleDateString()}に配信
</Text>
{/** biome-ignore lint/a11y/useMediaCaption: there is no caption available */}
<audio
className="w-full"
src={cast.mp3Url}
controls
onPlay={() => setIsPlayingPodcast(true)}
onPause={() => setIsPlayingPodcast(false)}
/>
</Box>
)}

<Box className="flex flex-col items-center gap-1">
<Text c="gray" size="sm">
過去の配信
</Text>
<Box className="flex gap-2">
<Anchor
href="https://podcasts.apple.com/podcast/samari-podcast/id1858742598"
target="_blank"
rel="noopener noreferrer"
>
<img
src={ApplePodcastsIcon}
className="size-7"
title="Apple Podcasts"
alt="Apple Podcasts"
/>
</Anchor>
<Anchor
href="https://open.spotify.com/show/0tRiLhIRG9gQjNPHRfPPa4"
target="_blank"
rel="noopener noreferrer"
>
<img
src={SpotifyIcon}
className="size-7"
alt="Spotify"
title="Spotify"
/>
</Anchor>
</Box>
</Box>
</Card>
</Box>

{activePostGroup && (
<Box className="flex items-center gap-2 pb-4 sm:pb-0">
<title>{title}</title>
Expand Down