Expose database projects on frontend
This commit is contained in:
parent
4fde0e6148
commit
867c23402f
4 changed files with 200 additions and 114 deletions
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 || []} />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue