Create favorites section (#84)
This commit is contained in:
parent
89f1dcc0d0
commit
d6139f2c43
9 changed files with 108 additions and 35 deletions
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -8648,9 +8648,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.31.1",
|
"version": "0.31.2",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.2.tgz",
|
||||||
"integrity": "sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==",
|
"integrity": "sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
|
|
@ -8659,7 +8659,7 @@
|
||||||
"detect-libc": "^2.0.1",
|
"detect-libc": "^2.0.1",
|
||||||
"node-addon-api": "^5.0.0",
|
"node-addon-api": "^5.0.0",
|
||||||
"prebuild-install": "^7.1.1",
|
"prebuild-install": "^7.1.1",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.8",
|
||||||
"simple-get": "^4.0.1",
|
"simple-get": "^4.0.1",
|
||||||
"tar-fs": "^2.1.1",
|
"tar-fs": "^2.1.1",
|
||||||
"tunnel-agent": "^0.6.0"
|
"tunnel-agent": "^0.6.0"
|
||||||
|
|
@ -8672,9 +8672,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp/node_modules/semver": {
|
"node_modules/sharp/node_modules/semver": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -16642,9 +16642,9 @@
|
||||||
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g=="
|
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g=="
|
||||||
},
|
},
|
||||||
"sharp": {
|
"sharp": {
|
||||||
"version": "0.31.1",
|
"version": "0.31.2",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.2.tgz",
|
||||||
"integrity": "sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==",
|
"integrity": "sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -16652,16 +16652,16 @@
|
||||||
"detect-libc": "^2.0.1",
|
"detect-libc": "^2.0.1",
|
||||||
"node-addon-api": "^5.0.0",
|
"node-addon-api": "^5.0.0",
|
||||||
"prebuild-install": "^7.1.1",
|
"prebuild-install": "^7.1.1",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.8",
|
||||||
"simple-get": "^4.0.1",
|
"simple-get": "^4.0.1",
|
||||||
"tar-fs": "^2.1.1",
|
"tar-fs": "^2.1.1",
|
||||||
"tunnel-agent": "^0.6.0"
|
"tunnel-agent": "^0.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,17 @@ const BookTile = 'a';
|
||||||
|
|
||||||
<BookTile
|
<BookTile
|
||||||
href={value.url}
|
href={value.url}
|
||||||
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', props.class]}
|
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', 'w-full', props.class]}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
class:list={['rounded-lg', 'shadow-md', 'object-cover']}
|
class:list={['rounded-lg', 'shadow-md', 'aspect-[3/4]', 'object-cover']}
|
||||||
src={value.cover}
|
src={value.cover}
|
||||||
aspectRatio="1.3"
|
aspectRatio={3 / 4}
|
||||||
|
width="auto"
|
||||||
alt={value.title}
|
alt={value.title}
|
||||||
format="webp"
|
format="webp"
|
||||||
/>
|
/>
|
||||||
<div class:list={['tile-content', 'gap-1']}>
|
<div class:list={['tile-content', 'gap-1', 'w-full']}>
|
||||||
<Typography class:list={['leading-5', 'hover:text-gray-900']} variant="tile-title">
|
<Typography class:list={['leading-5', 'hover:text-gray-900']} variant="tile-title">
|
||||||
{value.title}
|
{value.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ type Props = IconProps & Omit<TooltipProps, 'children'>;
|
||||||
|
|
||||||
const IconWithTooltip = ({ name, color, size, ...tooltipProps }: Props) => (
|
const IconWithTooltip = ({ name, color, size, ...tooltipProps }: Props) => (
|
||||||
<Tooltip {...tooltipProps}>
|
<Tooltip {...tooltipProps}>
|
||||||
<div>
|
<div className="cursor-pointer">
|
||||||
<Icon name={name} color={color} size={size} />
|
<Icon name={name} color={color} size={size} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,14 @@ const MediaTile = 'a';
|
||||||
|
|
||||||
<MediaTile
|
<MediaTile
|
||||||
href={value.url}
|
href={value.url}
|
||||||
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', props.class]}
|
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', 'w-full', props.class]}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
class:list={['rounded-lg', 'shadow-md', 'object-cover']}
|
class:list={['rounded-lg', 'shadow-md', 'aspect-square', 'object-cover']}
|
||||||
src={value.image}
|
src={value.image}
|
||||||
|
width="auto"
|
||||||
|
aspectRatio={1 / 1}
|
||||||
alt={value.title}
|
alt={value.title}
|
||||||
aspectRatio="1.1"
|
|
||||||
format="webp"
|
format="webp"
|
||||||
/>
|
/>
|
||||||
<div class:list={['tile-content', 'gap-1']}>
|
<div class:list={['tile-content', 'gap-1']}>
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,19 @@ const PersonTile = 'a';
|
||||||
|
|
||||||
<PersonTile
|
<PersonTile
|
||||||
href={value.url}
|
href={value.url}
|
||||||
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', props.class]}
|
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', 'w-full', props.class]}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
class:list={['rounded-lg', 'shadow-md', 'object-cover', 'transition', 'duration-300']}
|
class:list={['rounded-lg', 'shadow-md', 'transition', 'duration-300', 'aspect-square', 'object-cover']}
|
||||||
|
width="auto"
|
||||||
|
aspectRatio={1 / 1}
|
||||||
src={value.image}
|
src={value.image}
|
||||||
alt={value.name}
|
alt={value.name}
|
||||||
aspectRatio="1/1"
|
|
||||||
format="webp"
|
format="webp"
|
||||||
/>
|
/>
|
||||||
<div class:list={['tile-content', 'gap-1']}>
|
<div class:list={['tile-content', 'gap-1']}>
|
||||||
<Typography class:list={['leading-5', 'hover:text-gray-900']} variant="tile-title">
|
<Typography class:list={['leading-5', 'hover:text-gray-900']} variant="tile-title">
|
||||||
{value.name}
|
{value.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="tile-subtitle">
|
|
||||||
{value.name}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
</PersonTile>
|
</PersonTile>
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ const getVideoThumbnail = (url: string) => {
|
||||||
|
|
||||||
<VideoTile
|
<VideoTile
|
||||||
href={value.url}
|
href={value.url}
|
||||||
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', props.class]}
|
class:list={['flex', 'flex-col', 'gap-3', 'transition', 'duration-300', 'hover:translate-y-2', 'w-full', props.class]}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
class:list={['rounded-lg', 'shadow-md', 'object-cover']}
|
class:list={['rounded-lg', 'shadow-md', 'aspect-video', 'object-cover']}
|
||||||
src={getVideoThumbnail(value.url)}
|
src={getVideoThumbnail(value.url)}
|
||||||
width="auto"
|
width="auto"
|
||||||
aspectRatio="1.67"
|
aspectRatio={16 / 9}
|
||||||
alt={value.title}
|
alt={value.title}
|
||||||
format="webp"
|
format="webp"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,87 @@
|
||||||
---
|
---
|
||||||
|
import type { ComponentInstance } from 'astro';
|
||||||
|
|
||||||
|
import BookTile from '@/atoms/book-tile.astro';
|
||||||
|
import MediaTile from '@/atoms/media-tile.astro';
|
||||||
|
import PersonTile from '@/atoms/person-tile.astro';
|
||||||
import SectionCard from '@/atoms/section-card.astro';
|
import SectionCard from '@/atoms/section-card.astro';
|
||||||
import Typography from '@/atoms/typography.astro';
|
import Typography from '@/atoms/typography.astro';
|
||||||
|
import VideoTile from '@/atoms/video-tile.astro';
|
||||||
import type { Section } from '@/types/data';
|
import type { Section } from '@/types/data';
|
||||||
import type { FavoritesSection } from '@/types/favorites-section';
|
import type { Book, FavoritesSection, Media, Person, Video } from '@/types/favorites-section';
|
||||||
|
|
||||||
export interface Props extends FavoritesSection {}
|
export interface Props extends FavoritesSection {}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
config: { title },
|
config: { title },
|
||||||
|
books,
|
||||||
|
medias,
|
||||||
|
people,
|
||||||
|
videos,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
|
type Subsection = 'books' | 'medias' | 'people' | 'videos';
|
||||||
|
type SubsectionData = Book | Media | Person | Video;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required to avoid type casting
|
||||||
|
type SubsectionComponent = (_props: { value: any }) => ComponentInstance;
|
||||||
|
|
||||||
|
interface FavoritesSubsection<T extends SubsectionData> {
|
||||||
|
name: Subsection;
|
||||||
|
data: T[];
|
||||||
|
title: string;
|
||||||
|
columnsLayout: string;
|
||||||
|
Component: SubsectionComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const booksSubsection: FavoritesSubsection<Book> = {
|
||||||
|
name: 'books',
|
||||||
|
columnsLayout: 'grid-cols-4',
|
||||||
|
Component: BookTile,
|
||||||
|
...books,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mediasSubsection: FavoritesSubsection<Media> = {
|
||||||
|
name: 'medias',
|
||||||
|
columnsLayout: 'grid-cols-6',
|
||||||
|
Component: MediaTile,
|
||||||
|
...medias,
|
||||||
|
};
|
||||||
|
|
||||||
|
const peopleSubsection: FavoritesSubsection<Person> = {
|
||||||
|
name: 'people',
|
||||||
|
columnsLayout: 'grid-cols-6',
|
||||||
|
Component: PersonTile,
|
||||||
|
...people,
|
||||||
|
};
|
||||||
|
|
||||||
|
const videosSubsection: FavoritesSubsection<Video> = {
|
||||||
|
name: 'videos',
|
||||||
|
columnsLayout: 'grid-cols-3',
|
||||||
|
Component: VideoTile,
|
||||||
|
...videos,
|
||||||
|
};
|
||||||
|
|
||||||
|
const subsections = [booksSubsection, peopleSubsection, videosSubsection, mediasSubsection];
|
||||||
|
|
||||||
const section: Section = 'favorites';
|
const section: Section = 'favorites';
|
||||||
---
|
---
|
||||||
|
|
||||||
<SectionCard section={section}
|
<SectionCard section={section}>
|
||||||
><Typography variant="section-title" id={`${section}-heading`}>{title}</Typography>
|
<Typography variant="section-title" id={`${section}-heading`}>{title}</Typography>
|
||||||
|
<div class:list={['flex', 'flex-col', 'gap-16']}>
|
||||||
|
{
|
||||||
|
subsections.map(({ Component, data, name, columnsLayout, title: subsectionTitle }) => (
|
||||||
|
<div class:list={['flex', 'flex-col', 'gap-6']}>
|
||||||
|
<Typography variant="section-subtitle" id={`${section}-${name}-heading`}>
|
||||||
|
{subsectionTitle}
|
||||||
|
</Typography>
|
||||||
|
<div class:list={['grid', 'gap-8', columnsLayout]}>
|
||||||
|
{data.map((value) => (
|
||||||
|
<Component value={value} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const { seo, ...dataWithoutSeo } = data;
|
||||||
return (
|
return (
|
||||||
sectionData && (
|
sectionData && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
client:load
|
client:only="react"
|
||||||
title={sectionData.config.title}
|
title={sectionData.config.title}
|
||||||
icon={sectionData.config.icon}
|
icon={sectionData.config.icon}
|
||||||
section={key}
|
section={key}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import MainSection from '@/sections/main-section.astro';
|
||||||
import type { MainSection as MainSectionData } from '@/types/main-section';
|
import type { MainSection as MainSectionData } from '@/types/main-section';
|
||||||
|
|
||||||
const mainSectionData: MainSectionData = {
|
const mainSectionData: MainSectionData = {
|
||||||
|
config: {
|
||||||
|
title: 'Main Section',
|
||||||
|
icon: 'fa6-solid:user',
|
||||||
|
},
|
||||||
image: import('@/assets/my-image.jpeg'),
|
image: import('@/assets/my-image.jpeg'),
|
||||||
fullName: 'Mark Freeman',
|
fullName: 'Mark Freeman',
|
||||||
role: 'Senior React Developer',
|
role: 'Senior React Developer',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue