added deploy watch, pull script. minor refactors
This commit is contained in:
@@ -19,6 +19,7 @@ export default function ModCard({ mod, visible = true }: Props) {
|
||||
href={mod.link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
title={`Open ${mod.linkLabel ?? "repository"} in a new tab`}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { Project } from "@/data/content";
|
||||
import { type Project, imageSrc, imageDescription } from "@/data/content";
|
||||
import SimpleGallery from "./SimpleGallery";
|
||||
import Link from "next/link";
|
||||
import "../styles/project-card.css";
|
||||
@@ -144,7 +144,7 @@ export default function ProjectCard({ project, visible, selected = false, setSel
|
||||
textAlign: "center",
|
||||
width: "10em",
|
||||
borderRadius: "var(--radius-md)",
|
||||
}} href={"/project/"+project.slug} >See More</Link>
|
||||
}} href={"/project/"+project.slug} title={`See more details about ${project.title}`}>See More</Link>
|
||||
|
||||
<Link
|
||||
className="expand-on-hover-button"
|
||||
@@ -160,7 +160,7 @@ export default function ProjectCard({ project, visible, selected = false, setSel
|
||||
textAlign: "center",
|
||||
width: "10em",
|
||||
borderRadius: "var(--radius-md)",
|
||||
}} target="_blank" href={project.link} >Link</Link>
|
||||
}} target="_blank" href={project.link} title={`Open ${project.link} in a new tab`}>Link</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -168,8 +168,9 @@ export default function ProjectCard({ project, visible, selected = false, setSel
|
||||
}
|
||||
|
||||
function imgComponent(){
|
||||
const image = project.images![0];
|
||||
return (
|
||||
<img src={`/${project.images?.[0]}`} alt={`${project.title} screenshot`} width={800} height={180} style={{width: "100%", borderRadius: 4, marginBottom: 16, objectFit: "cover", height: 180}} />
|
||||
<img src={`/${imageSrc(image)}`} alt={imageDescription(image) ?? `${project.title} screenshot`} width={800} height={180} style={{width: "100%", borderRadius: 4, marginBottom: 16, objectFit: "cover", height: 180}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,21 @@ import Image from "next/image";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import '@/styles/gallery.css';
|
||||
import { type GalleryImage, imageSrc, imageDescription } from "@/data/content";
|
||||
|
||||
type GallerySize = "small" | "medium" | "large";
|
||||
type Props = {
|
||||
images: string[];
|
||||
images: GalleryImage[];
|
||||
videos?: string[];
|
||||
title: string;
|
||||
gallerySize?: GallerySize;
|
||||
};
|
||||
|
||||
export default function SimpleGallery({
|
||||
images,
|
||||
videos = [],
|
||||
title
|
||||
title,
|
||||
gallerySize = "medium"
|
||||
}: Props) {
|
||||
|
||||
const [active, setActive] = useState(0);
|
||||
@@ -28,10 +32,24 @@ export default function SimpleGallery({
|
||||
const totalMedia = images.length + videos.length;
|
||||
const isVideo = active >= images.length;
|
||||
const videoIndex = active - images.length;
|
||||
const currentSrc = isVideo ? videos[videoIndex] : images[active];
|
||||
const currentSrc = isVideo ? videos[videoIndex] : imageSrc(images[active]);
|
||||
const currentDescription = isVideo ? undefined : imageDescription(images[active]);
|
||||
|
||||
if (totalMedia === 0) return null;
|
||||
|
||||
function getGallerySize() {
|
||||
switch (gallerySize) {
|
||||
case "small":
|
||||
return { maxHeight: 150 };
|
||||
case "medium":
|
||||
return { maxHeight: 250 };
|
||||
case "large":
|
||||
return { maxHeight: 500 };
|
||||
default:
|
||||
return { maxHeight: 250 };
|
||||
}
|
||||
}
|
||||
|
||||
function handleFullscreenClose() {
|
||||
setIsEnlarged(false);
|
||||
}
|
||||
@@ -94,8 +112,8 @@ export default function SimpleGallery({
|
||||
const imageStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
borderRadius: 4,
|
||||
objectFit: "cover",
|
||||
maxHeight: 250,
|
||||
objectFit: "contain",
|
||||
maxHeight: getGallerySize().maxHeight,
|
||||
display: "block",
|
||||
transition: "all 0.8s ease",
|
||||
zIndex: 1,
|
||||
@@ -104,7 +122,7 @@ export default function SimpleGallery({
|
||||
const videoStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
borderRadius: 4,
|
||||
maxHeight: 250,
|
||||
maxHeight: getGallerySize().maxHeight,
|
||||
display: "block",
|
||||
transition: "all 0.8s ease",
|
||||
};
|
||||
@@ -126,7 +144,8 @@ export default function SimpleGallery({
|
||||
<Image
|
||||
id="gallery-main-image"
|
||||
src={`/${currentSrc}`}
|
||||
alt={`${title} screenshot ${active + 1}`}
|
||||
alt={currentDescription ?? `${title} screenshot ${active + 1}`}
|
||||
title={currentDescription}
|
||||
width={800}
|
||||
height={600}
|
||||
onClick={() => setIsEnlarged(true)}
|
||||
@@ -216,11 +235,17 @@ export default function SimpleGallery({
|
||||
) : (
|
||||
<Image
|
||||
src={`/${currentSrc}`}
|
||||
alt={`${title} fullscreen ${active + 1}`}
|
||||
alt={currentDescription ?? `${title} fullscreen ${active + 1}`}
|
||||
title={currentDescription}
|
||||
width={1600}
|
||||
height={1200}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
// Auto sizing keeps the element's box tight to the visible image;
|
||||
// otherwise the letterboxed area inside the box swallows taps
|
||||
// meant for the darkened background.
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
maxWidth: "95vw",
|
||||
maxHeight: "90vh",
|
||||
borderRadius: 8,
|
||||
@@ -245,27 +270,40 @@ export default function SimpleGallery({
|
||||
marginTop: 8,
|
||||
flexWrap: "wrap" }}>
|
||||
|
||||
{images.map((src, i) => (
|
||||
<Image
|
||||
key={`thumb-img-${i}`}
|
||||
src={`/${src}`}
|
||||
alt={`${title} thumbnail ${i + 1}`}
|
||||
width={48}
|
||||
height={36}
|
||||
onClick={() => handleThumbnailClick(i)}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 36,
|
||||
objectFit: "cover",
|
||||
borderRadius: 3,
|
||||
cursor: "pointer",
|
||||
border: `2px solid ${i === active ? "var(--accent)" : "var(--border)"}`,
|
||||
opacity: i === active ? 1 : 0.6,
|
||||
transition: "all 0.15s ease",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{images.map((image, i) => {
|
||||
const description = imageDescription(image);
|
||||
const isActive = i === active;
|
||||
return (
|
||||
<button
|
||||
key={`thumb-img-${i}`}
|
||||
className={`gallery-thumb${isActive ? " is-active" : ""}`}
|
||||
onClick={() => handleThumbnailClick(i)}
|
||||
title={description ?? `Show ${title} screenshot ${i + 1}`}
|
||||
>
|
||||
<Image
|
||||
src={`/${imageSrc(image)}`}
|
||||
alt={description ?? `${title} thumbnail ${i + 1}`}
|
||||
width={48}
|
||||
height={36}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 36,
|
||||
objectFit: "cover",
|
||||
borderRadius: 3,
|
||||
border: `2px solid ${isActive ? "var(--accent)" : "var(--border)"}`,
|
||||
opacity: isActive ? 1 : 0.6,
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
/>
|
||||
<span className="gallery-thumb-magnifier" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="7" cy="7" r="4.5" strokeWidth="1.75" />
|
||||
<path d="M10.5 10.5L14 14" strokeWidth="1.75" strokeLinecap="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{videos.map((_, i) => {
|
||||
const mediaIndex = images.length + i;
|
||||
@@ -332,6 +370,8 @@ export default function SimpleGallery({
|
||||
borderRadius: 7,
|
||||
border: `1px solid ${mediaHovered ? "var(--border-strong)" : "transparent"}`,
|
||||
transition: "border-color 0.15s ease",
|
||||
backgroundColor: "var(--overlay, #1a1a1a)",
|
||||
boxShadow: "var(--shadow-sm)"
|
||||
}}
|
||||
>
|
||||
{inlineMedia()}
|
||||
|
||||
@@ -15,16 +15,15 @@ const ThemeSwitch = () => {
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
const next = () => {
|
||||
const i = themes.indexOf(theme as (typeof themes)[number])
|
||||
setTheme(themes[(i + 1) % themes.length])
|
||||
}
|
||||
const nextTheme = themes[(themes.indexOf(theme as (typeof themes)[number]) + 1) % themes.length]
|
||||
const next = () => setTheme(nextTheme)
|
||||
const label = `Theme: ${theme}. Click to switch to ${nextTheme}.`
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={next}
|
||||
aria-label={`Theme: ${theme}`}
|
||||
title={`Theme: ${theme}`}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 'var(--space-4)',
|
||||
|
||||
Reference in New Issue
Block a user