feat: Add screenshot gallery feature (#194)
Co-authored-by: Konrad Szwarc <konrad.szwarc.dev@gmail.com>
This commit is contained in:
parent
f51c4044b9
commit
4f137e2a92
12 changed files with 125 additions and 11 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
|||
"locales-ts": "1.0.0",
|
||||
"marked": "4.2.12",
|
||||
"move-file-cli": "3.0.0",
|
||||
"photoswipe": "5.3.4",
|
||||
"postcss": "8.4.21",
|
||||
"prettier": "2.8.3",
|
||||
"prettier-plugin-astro": "0.8.0",
|
||||
|
|
@ -6293,6 +6294,15 @@
|
|||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/photoswipe": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz",
|
||||
"integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
|
@ -13536,6 +13546,12 @@
|
|||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"photoswipe": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz",
|
||||
"integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw==",
|
||||
"dev": true
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
"locales-ts": "1.0.0",
|
||||
"marked": "4.2.12",
|
||||
"move-file-cli": "3.0.0",
|
||||
"photoswipe": "5.3.4",
|
||||
"postcss": "8.4.21",
|
||||
"prettier": "2.8.3",
|
||||
"prettier-plugin-astro": "0.8.0",
|
||||
|
|
|
|||
BIN
src/assets/portfolio/project-1-screenshot-1.jpg
Normal file
BIN
src/assets/portfolio/project-1-screenshot-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
src/assets/portfolio/project-1-screenshot-2.jpg
Normal file
BIN
src/assets/portfolio/project-1-screenshot-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/portfolio/project-1-screenshot-3.jpg
Normal file
BIN
src/assets/portfolio/project-1-screenshot-3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
|
|
@ -155,7 +155,7 @@ export const demo = createLinkFactory({
|
|||
|
||||
export const mockups = createLinkFactory({
|
||||
name: 'Mockups',
|
||||
icon: 'fa6-solid:image',
|
||||
icon: 'fa6-solid:paintbrush',
|
||||
});
|
||||
|
||||
export const repository = createLinkFactory({
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ const portfolioSectionData = {
|
|||
slug: 'projects',
|
||||
icon: 'fa6-solid:rocket',
|
||||
visible: true,
|
||||
screenshots: {
|
||||
title: 'Screenshots',
|
||||
icon: 'fa6-solid:images',
|
||||
},
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
|
|
@ -40,6 +44,11 @@ const portfolioSectionData = {
|
|||
{ label: 'Demo', value: 'https://golden-bulls-d73jd7.netlify.app', url: '#' },
|
||||
{ label: 'Repository', value: 'https://github.com/mark-freeman/golden-bulls', url: '#' },
|
||||
],
|
||||
screenshots: [
|
||||
{ src: import('@/assets/portfolio/project-1-screenshot-1.jpg'), alt: 'First screenshot' },
|
||||
{ src: import('@/assets/portfolio/project-1-screenshot-2.jpg'), alt: 'Second screenshot' },
|
||||
{ src: import('@/assets/portfolio/project-1-screenshot-3.jpg'), alt: 'Third screenshot' },
|
||||
],
|
||||
description:
|
||||
'In tristique vulputate augue vel egestas. Quisque ac imperdiet tortor, at lacinia ex. Duis vel ex hendrerit, commodo odio sed, aliquam enim. Ut arcu nulla, tincidunt eget arcu eget, molestie vulputate nisi. Nunc malesuada leo et est iaculis facilisis.',
|
||||
tagsList: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
import type { DateRange, Photo, LabelledValue, LinkButton, Section, TagsList } from '../shared';
|
||||
import type { DateRange, Photo, LabelledValue, LinkButton, Section, TagsList, IconName } from '../shared';
|
||||
|
||||
interface Screenshot {
|
||||
/**
|
||||
* [WEB] Source of the screenshot.
|
||||
*/
|
||||
src: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Alt text for the screenshot.
|
||||
*/
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
/**
|
||||
|
|
@ -39,6 +51,11 @@ export interface Project {
|
|||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* [WEB] Screenshots of the project.
|
||||
*/
|
||||
screenshots?: Screenshot[];
|
||||
|
||||
/**
|
||||
* Any information that you want to highlight.
|
||||
* We recommend to describe the technologies used in the project.
|
||||
|
|
@ -56,4 +73,21 @@ export interface PortfolioSection extends Section {
|
|||
* List of your projects in a chronological order. Start with the most recent one.
|
||||
*/
|
||||
projects: Project[];
|
||||
|
||||
config: Section['config'] & {
|
||||
/**
|
||||
* [WEB] Configuration of the button that displays project's screenshots.
|
||||
*/
|
||||
screenshots?: {
|
||||
/**
|
||||
* [WEB] Icon displayed within the button.
|
||||
*/
|
||||
icon?: IconName;
|
||||
|
||||
/**
|
||||
* [WEB] Title displayed when hovering the button.
|
||||
*/
|
||||
title?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,29 @@
|
|||
import type { LinkButton } from '@/types/shared';
|
||||
import Icon from './icon.astro';
|
||||
|
||||
export interface Props extends LinkButton {}
|
||||
export interface Props extends Omit<LinkButton, 'url'>, Omit<astroHTML.JSX.HTMLAttributes, 'slot'> {
|
||||
url?: LinkButton['url'];
|
||||
as?: 'a' | 'button';
|
||||
}
|
||||
|
||||
const { name, icon, url } = Astro.props;
|
||||
const { name, icon, url, as, ...props } = Astro.props;
|
||||
|
||||
const classes = /* tw */ {
|
||||
main: 'flex items-center justify-center w-7 h-7 rounded text-gray-400 bg-gray-100 dark:bg-gray-600 dark:text-gray-200',
|
||||
active: 'active:translate-y-px',
|
||||
focus: 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
};
|
||||
|
||||
const Element = as || 'a';
|
||||
---
|
||||
|
||||
<a
|
||||
<Element
|
||||
aria-label={name}
|
||||
href={url}
|
||||
target="_blank"
|
||||
data-tooltip={name}
|
||||
class:list={[classes.main, classes.active, classes.focus]}
|
||||
{...props}
|
||||
>
|
||||
<Icon name={icon} size={16} />
|
||||
</a>
|
||||
</Element>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ const { config, projects } = Astro.props;
|
|||
|
||||
<SectionCard {...config}>
|
||||
<DividedList>
|
||||
{projects.flatMap((project) => [<Project {...project} />, <Divider />])}
|
||||
{projects.flatMap((project) => [<Project {...project} screenshotsConfig={config.screenshots} />, <Divider />])}
|
||||
</DividedList>
|
||||
</SectionCard>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
import type { Project } from '@/types/sections/portfolio-section.types';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PortfolioSection, Project } from '@/types/sections/portfolio-section.types';
|
||||
import Description from '@/web/components/description.astro';
|
||||
import LabelledValue from '@/web/components/labelled-value.astro';
|
||||
import LinkButton from '@/web/components/link-button.astro';
|
||||
|
|
@ -8,11 +9,18 @@ import TagsList from '@/web/components/tags-list.astro';
|
|||
import Timestamp from '@/web/components/timestamp.astro';
|
||||
import Typography from '@/web/components/typography.astro';
|
||||
|
||||
export interface Props extends Project {}
|
||||
export interface Props extends Project {
|
||||
screenshotsConfig?: PortfolioSection['config']['screenshots'];
|
||||
}
|
||||
|
||||
const { dates, description, details, image, links, name, tagsList } = Astro.props;
|
||||
const { dates, description, details, image, links, name, tagsList, screenshots, screenshotsConfig } = Astro.props;
|
||||
|
||||
const alt = `${name} project thumbnail`;
|
||||
|
||||
const galleryId = nanoid(8);
|
||||
const hasScreenshots = screenshots?.length && screenshots.length > 0;
|
||||
const screenshotsIcon = screenshotsConfig?.icon || 'fa6-solid:image';
|
||||
const screenshotsTooltip = screenshotsConfig?.title || 'Screenshots';
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
|
|
@ -38,6 +46,11 @@ const alt = `${name} project thumbnail`;
|
|||
</div>
|
||||
<div class="flex gap-2">
|
||||
{links.map((link) => <LinkButton {...link} />)}
|
||||
{
|
||||
hasScreenshots && (
|
||||
<LinkButton icon={screenshotsIcon} name={screenshotsTooltip} as="button" data-gallery={galleryId} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -49,4 +62,39 @@ const alt = `${name} project thumbnail`;
|
|||
<Description content={description} class="col-span-3 col-start-1" />
|
||||
</div>
|
||||
<TagsList {...tagsList} />
|
||||
<div class="hidden" id={galleryId}>
|
||||
{screenshots?.map((screenshot) => <Photo {...screenshot} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import type { PhotoSwipeOptions, DataSource } from 'photoswipe';
|
||||
|
||||
const buttons = [...document.querySelectorAll('[data-gallery]')] as HTMLButtonElement[];
|
||||
|
||||
if (buttons.length > 0) {
|
||||
import('photoswipe/style.css');
|
||||
|
||||
const getOptionsForButton = (button: HTMLButtonElement): PhotoSwipeOptions => {
|
||||
const galleryId = String(button.dataset.gallery);
|
||||
const galleryWrapper = document.getElementById(galleryId) as HTMLElement;
|
||||
const screenshots = [...galleryWrapper.children] as HTMLImageElement[];
|
||||
const dataSource: DataSource = screenshots.map((img) => ({
|
||||
src: img.src,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
alt: img.alt,
|
||||
}));
|
||||
|
||||
return { dataSource, showHideAnimationType: 'none', index: 0 };
|
||||
};
|
||||
|
||||
import('photoswipe').then(({ default: PhotoSwipe }) => {
|
||||
buttons.forEach((button) =>
|
||||
button.addEventListener('click', () => {
|
||||
new PhotoSwipe(getOptionsForButton(button)).init();
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const { author, content, image, links, relation } = Astro.props;
|
|||
<div class="flex w-full flex-col gap-4">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<Photo src={image} alt={author} class="w-14 h-14 rounded-lg" width={108} height={108} />
|
||||
<Photo src={image} alt={author} class="w-14 h-14 rounded-lg" width={112} height={112} />
|
||||
<div>
|
||||
<Typography variant="item-title">{author}</Typography>
|
||||
<Typography variant="item-subtitle">{relation}</Typography>
|
||||
|
|
|
|||
Loading…
Reference in a new issue