[#30] Stubbed out a functional landing page.

This commit is contained in:
Justin Walrath 2025-05-15 22:40:35 -04:00
parent f8afcb78fb
commit f99c04f005
7 changed files with 114 additions and 3 deletions

1
public/icons/pause.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6 18.4V5.6C6 5.26863 6.26863 5 6.6 5H9.4C9.73137 5 10 5.26863 10 5.6V18.4C10 18.7314 9.73137 19 9.4 19H6.6C6.26863 19 6 18.7314 6 18.4Z" stroke="#000000" stroke-width="1.5"></path><path d="M14 18.4V5.6C14 5.26863 14.2686 5 14.6 5H17.4C17.7314 5 18 5.26863 18 5.6V18.4C18 18.7314 17.7314 19 17.4 19H14.6C14.2686 19 14 18.7314 14 18.4Z" stroke="#000000" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 566 B

1
public/icons/play.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6.90588 4.53682C6.50592 4.2998 6 4.58808 6 5.05299V18.947C6 19.4119 6.50592 19.7002 6.90588 19.4632L18.629 12.5162C19.0211 12.2838 19.0211 11.7162 18.629 11.4838L6.90588 4.53682Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 458 B

1
public/icons/search.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M17 17L21 21" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 11C3 15.4183 6.58172 19 11 19C13.213 19 15.2161 18.1015 16.6644 16.6493C18.1077 15.2022 19 13.2053 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@ -21,7 +21,7 @@ export default function PodcastCard({
return (
<div className="w-full min-h-[150px] shadow-md rounded-sm overflow-hidden flex m-4 mb-1 bg-purple-200">
<div className="w-[150px] h-full max-h-[232px] bg-purple-100 flex-shrink-0 relative">
<Image unoptimized src={imageUrl ?? `${process.env.NEXT_PUBLIC_API_BASE_URL}/icons/podcast.svg`} alt={title ?? ''} layout="fill" objectFit="contain" />
<Image src={imageUrl ?? `${process.env.NEXT_PUBLIC_API_BASE_URL}/icons/podcast.svg`} alt={title ?? ''} layout="fill" className="object-contain" />
</div>
<div className="p-4 flex-grow flex flex-col">

View File

@ -1,10 +1,101 @@
"use client";
import Image from 'next/image';
import Main from './layout/main';
import { useRef, useState, useEffect } from 'react';
import { PodcastDto } from '@/common/dtos/podcastDto';
import { useRouter } from 'next/navigation';
export default function Home() {
const inputRef = useRef<HTMLInputElement>(null),
[podcasts, setPodcasts] = useState<PodcastDto[]>([]),
router = useRouter(),
handleSearchClick = () => {
alert(inputRef.current?.value);
},
handlePodcastClick = (podcast: PodcastDto) => {
if (podcast.streamId != null) {
router.push(`${process.env.NEXT_PUBLIC_API_BASE_URL}/${podcast.streamId}/podcasts`); // TODO: This should eventually go to the individual player page.
}
};
useEffect(() => {
fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/podcasts/random?limit=12`)
.then(res => res.json())
.then(setPodcasts)
.catch(() => setPodcasts([]));
}, []);
return (
<Main>
<h1 className='text-purple-200'>Welcome</h1>
<a className='text-purple-200' href={`${process.env.NEXT_PUBLIC_API_BASE_URL}/f60236c3-81dd-47ba-b0ae-afd9229ac6f2/podcasts`}>Go to podcast list.</a>
<div className="w-full flex justify-center mt-8">
<div className="w-[100px] h-[100px] flex items-center justify-center">
<Image
src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/site-icon.svg`}
alt="Site Icon"
width={100}
height={100}
className="object-contain"
priority
/>
</div>
</div>
<div className="w-full flex justify-center mt-8">
<div className="relative w-[320px]">
<input
ref={inputRef}
type='text'
placeholder='Search…'
className='w-full pr-12 pl-4 py-4 rounded-lg bg-purple-200 text-base outline-none border border-purple-200'
/>
<button
type="button"
onClick={handleSearchClick}
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded hover:bg-purple-100 transition"
tabIndex={0}
>
<Image
src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/icons/search.svg`}
alt="Search"
width={24}
height={24}
/>
</button>
</div>
</div>
<div className="w-full flex justify-center mt-8">
<div className="w-[500px] flex flex-col items-center">
<h1 className="text-2xl font-bold text-purple-200 text-center">
Not sure what you are looking for?
</h1>
<p className="text-sm text-purple-200 mt-2 text-center">
Get started by selecting something of interest:
</p>
</div>
</div>
<div className="w-full px-8 mt-8">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
{podcasts.map(podcast => (
<button
key={podcast.streamId}
type='button'
onClick={() => handlePodcastClick(podcast)}
className="aspect-square bg-purple-100 rounded-lg flex items-center justify-center overflow-hidden shadow focus:outline-none focus:ring-2 focus:ring-purple-400 transition"
title={podcast.title}
>
<Image
src={podcast.imageUrl || `${process.env.NEXT_PUBLIC_API_BASE_URL}/icons/podcast.svg`}
alt={podcast.title ?? ''}
width={200}
height={200}
className="object-cover w-full h-full"
/>
</button>
))}
</div>
</div>
</Main>
);
}

View File

@ -62,6 +62,12 @@ export const getPodcasts = async (stream: string) => {
return db.data?.podcasts.filter((podcast) => podcast.streamId === stream) ?? [];
};
export const getRandomPodcasts = async (limit: number = 20) => {
const podcasts = db.data?.podcasts ?? [];
const shuffled = podcasts.slice().sort(() => Math.random() - 0.5);
return shuffled.slice(0, limit);
};
export const publishPodcast = async (podcast: PodcastDto) => {
await queueWrite(async () => {
db.data?.podcasts.push(podcast);

View File

@ -0,0 +1,11 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getRandomPodcasts } from '@/common/data/db';
import { PodcastDto } from '@/common/dtos/podcastDto';
import { preparePodcastItem } from '@/common/helpers/data';
export default async function handler(req: NextApiRequest, res: NextApiResponse<PodcastDto[]>) {
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
podcasts = (await getRandomPodcasts(limit)).map(preparePodcastItem);
res.status(200).json(podcasts);
}