Expose database projects on frontend

This commit is contained in:
Justin Edmund 2025-05-29 21:07:04 -07:00
parent 4fde0e6148
commit 867c23402f
4 changed files with 200 additions and 114 deletions

View file

@ -1,26 +1,89 @@
<script lang="ts">
import SVGHoverEffect from '$components/SVGHoverEffect.svelte'
import type { SvelteComponent } from 'svelte'
import { onMount } from 'svelte'
export let SVGComponent: typeof SvelteComponent
export let backgroundColor: string
export let name: string
export let description: string
export let highlightColor: string
export let index: number = 0
interface Props {
logoUrl: string | null
backgroundColor: string
name: string
description: string
highlightColor: string
index?: number
}
$: isEven = index % 2 === 0
let { logoUrl, backgroundColor, name, description, highlightColor, index = 0 }: Props = $props()
const isEven = $derived(index % 2 === 0)
// Create highlighted description
$: highlightedDescription = description.replace(
new RegExp(`(${name})`, 'gi'),
`<span style="color: ${highlightColor};">$1</span>`
const highlightedDescription = $derived(
description.replace(
new RegExp(`(${name})`, 'gi'),
`<span style="color: ${highlightColor};">$1</span>`
)
)
// 3D tilt effect
let cardElement: HTMLDivElement
let isHovering = false
let transform = ''
let logoElement: HTMLElement
let isHovering = $state(false)
let transform = $state('')
let svgContent = $state('')
// Logo bounce effect
let logoTransform = $state('')
let velocity = { x: 0, y: 0 }
let position = { x: 0, y: 0 }
let animationFrame: number
const maxMovement = 10
const bounceDamping = 0.2
const friction = 0.85
onMount(async () => {
// Load SVG content
if (logoUrl) {
try {
const response = await fetch(logoUrl)
if (response.ok) {
const text = await response.text()
const parser = new DOMParser()
const doc = parser.parseFromString(text, 'image/svg+xml')
const svgElement = doc.querySelector('svg')
if (svgElement) {
svgElement.removeAttribute('width')
svgElement.removeAttribute('height')
svgContent = svgElement.outerHTML
}
}
} catch (error) {
console.error('Failed to load SVG:', error)
}
}
return () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame)
}
}
})
function updateLogoPosition() {
velocity.x *= friction
velocity.y *= friction
position.x += velocity.x
position.y += velocity.y
// Constrain position
position.x = Math.max(-maxMovement, Math.min(maxMovement, position.x))
position.y = Math.max(-maxMovement, Math.min(maxMovement, position.y))
logoTransform = `translate(${position.x}px, ${position.y}px)`
if (Math.abs(velocity.x) > 0.01 || Math.abs(velocity.y) > 0.01) {
animationFrame = requestAnimationFrame(updateLogoPosition)
}
}
function handleMouseMove(e: MouseEvent) {
if (!cardElement || !isHovering) return
@ -32,10 +95,31 @@
const centerX = rect.width / 2
const centerY = rect.height / 2
const rotateX = ((y - centerY) / centerY) * -4 // -4 to 4 degrees
const rotateY = ((x - centerX) / centerX) * 4 // -4 to 4 degrees
// 3D tilt for card
const rotateX = ((y - centerY) / centerY) * -4
const rotateY = ((x - centerX) / centerX) * 4
transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.014, 1.014, 1.014)`
// Logo movement
if (logoElement) {
const logoRect = logoElement.getBoundingClientRect()
const logoCenterX = logoRect.left + logoRect.width / 2 - rect.left
const logoCenterY = logoRect.top + logoRect.height / 2 - rect.top
const deltaX = x - logoCenterX
const deltaY = y - logoCenterY
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (distance < 100) {
const force = (100 - distance) / 100
velocity.x -= (deltaX / distance) * force * bounceDamping
velocity.y -= (deltaY / distance) * force * bounceDamping
if (!animationFrame) {
animationFrame = requestAnimationFrame(updateLogoPosition)
}
}
}
}
function handleMouseEnter() {
@ -45,6 +129,15 @@
function handleMouseLeave() {
isHovering = false
transform = 'perspective(1000px) rotateX(0) rotateY(0) scale3d(1, 1, 1)'
// Reset logo position
velocity = { x: 0, y: 0 }
position = { x: 0, y: 0 }
logoTransform = ''
if (animationFrame) {
cancelAnimationFrame(animationFrame)
animationFrame = 0
}
}
</script>
@ -56,14 +149,18 @@
on:mouseleave={handleMouseLeave}
style="transform: {transform};"
>
<div class="project-logo">
<SVGHoverEffect
{SVGComponent}
{backgroundColor}
maxMovement={10}
containerHeight="80px"
bounceDamping={0.2}
/>
<div class="project-logo" style="background-color: {backgroundColor}">
{#if svgContent}
<div
bind:this={logoElement}
class="logo-svg"
style="transform: {logoTransform}"
>
{@html svgContent}
</div>
{:else if logoUrl}
<img src={logoUrl} alt="{name} logo" class="logo-image" />
{/if}
</div>
<div class="project-content">
<p class="project-description">{@html highlightedDescription}</p>
@ -101,19 +198,31 @@
flex-shrink: 0;
width: 80px;
height: 80px;
border-radius: $unit-2x;
display: flex;
align-items: center;
justify-content: center;
padding: $unit-2x;
box-sizing: border-box;
:global(.svg-container) {
width: 80px !important;
height: 80px !important;
border-radius: $unit-2x;
.logo-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.logo-svg {
display: flex;
align-items: center;
justify-content: center;
}
width: 100%;
height: 100%;
transition: transform 0.15s ease-out;
:global(svg) {
width: 48px !important;
height: 48px !important;
:global(svg) {
width: 48px;
height: 48px;
}
}
}
@ -139,16 +248,6 @@
.project-logo {
width: 60px;
height: 60px;
:global(.svg-container) {
width: 60px !important;
height: 60px !important;
}
:global(svg) {
width: 36px !important;
height: 36px !important;
}
}
}
</style>
</style>

View file

@ -1,53 +1,12 @@
<script lang="ts">
import ProjectItem from '$components/ProjectItem.svelte'
import type { Project } from '$lib/types/project'
import MaitsuLogo from '$illos/logo-maitsu.svg?component'
import SlackLogo from '$illos/logo-slack.svg?component'
import FigmaLogo from '$illos/logo-figma.svg?component'
import PinterestLogo from '$illos/logo-pinterest.svg?component'
import SVGHoverEffect from '$components/SVGHoverEffect.svelte'
interface Project {
SVGComponent: typeof SvelteComponent
backgroundColor: string
name: string
description: string
highlightColor: string
interface Props {
projects: Project[]
}
const projects: Project[] = [
{
SVGComponent: MaitsuLogo,
backgroundColor: '#FFF7EA',
name: 'Maitsu',
description: 'Maitsu is a hobby journal that helps people make something new every week.',
highlightColor: '#F77754'
},
{
SVGComponent: SlackLogo,
backgroundColor: '#4a154b',
name: 'Slack',
description:
'At Slack, I helped redefine strategy for Workflows and other features in under the automation umbrella.',
highlightColor: '#611F69'
},
{
SVGComponent: FigmaLogo,
backgroundColor: '#2c2c2c',
name: 'Figma',
description:
'At Figma, I designed features and led R&D and strategy for the nascent prototyping team.',
highlightColor: '#0ACF83'
},
{
SVGComponent: PinterestLogo,
backgroundColor: '#f7f7f7',
name: 'Pinterest',
description:
'At Pinterest, I was the first product design hired, and touched almost every part of the product.',
highlightColor: '#CB1F27'
}
]
let { projects = [] }: Props = $props()
</script>
<section class="projects">
@ -64,9 +23,21 @@
</p>
</div>
</li>
{#if projects.length === 0}
<li>
<div class="no-projects">No projects found</div>
</li>
{/if}
{#each projects as project, index}
<li>
<ProjectItem {...project} {index} />
<ProjectItem
logoUrl={project.logoUrl}
backgroundColor={project.backgroundColor || '#f7f7f7'}
name={project.title}
description={project.description || ''}
highlightColor={project.highlightColor || '#333'}
{index}
/>
</li>
{/each}
</ul>
@ -114,4 +85,11 @@
color: #d0290d;
}
}
</style>
.no-projects {
padding: $unit-3x;
text-align: center;
color: $grey-40;
font-family: 'cstd', 'Helvetica Neue', Arial, sans-serif;
}
</style>

View file

@ -1,5 +1,8 @@
<script lang="ts">
import ProjectList from '$components/ProjectList.svelte'
import type { PageData } from './$types'
let { data } = $props<{ data: PageData }>()
</script>
<ProjectList />
<ProjectList projects={data?.projects || []} />

View file

@ -1,36 +1,34 @@
import type { PageLoad } from './$types'
import type { Album } from '$lib/types/lastfm'
import type { Project } from '$lib/types/project'
export const load: PageLoad = async ({ fetch }) => {
try {
// const [albums, steamGames, psnGames] = await Promise.all([
const [albums] = await Promise.all([
fetchRecentAlbums(fetch)
// fetchRecentSteamGames(fetch),
// fetchRecentPSNGames(fetch)
])
// Fetch albums first
let albums: Album[] = []
try {
albums = await fetchRecentAlbums(fetch)
} catch (albumError) {
console.error('Error fetching albums:', albumError)
}
// Fetch projects
let projectsData = { projects: [] as Project[], pagination: null }
try {
projectsData = await fetchProjects(fetch)
} catch (projectError) {
console.error('Error fetching projects:', projectError)
}
// const response = await fetch('/api/giantbomb', {
// method: 'POST',
// body: JSON.stringify({ games: psnGames }),
// headers: {
// 'Content-Type': 'application/json'
// }
// })
// const games = await response.json()
return {
albums
// games: games,
// steamGames: steamGames,
// psnGames: psnGames
albums,
projects: projectsData.projects || []
}
} catch (err) {
console.error('Error fetching data:', err)
console.error('Error in load function:', err)
return {
albums: [],
games: [],
error: err instanceof Error ? err.message : 'An unknown error occurred'
projects: []
}
}
}
@ -57,3 +55,11 @@ async function fetchRecentPSNGames(fetch: typeof window.fetch): Promise<Serializ
}
return await response.json()
}
async function fetchProjects(fetch: typeof window.fetch): Promise<{ projects: Project[]; pagination: any }> {
const response = await fetch('/api/projects?status=published')
if (!response.ok) {
throw new Error(`Failed to fetch projects: ${response.status}`)
}
return await response.json()
}