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">
|
<script lang="ts">
|
||||||
import SVGHoverEffect from '$components/SVGHoverEffect.svelte'
|
import { onMount } from 'svelte'
|
||||||
import type { SvelteComponent } from 'svelte'
|
|
||||||
|
|
||||||
export let SVGComponent: typeof SvelteComponent
|
interface Props {
|
||||||
export let backgroundColor: string
|
logoUrl: string | null
|
||||||
export let name: string
|
backgroundColor: string
|
||||||
export let description: string
|
name: string
|
||||||
export let highlightColor: string
|
description: string
|
||||||
export let index: number = 0
|
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
|
// Create highlighted description
|
||||||
$: highlightedDescription = description.replace(
|
const highlightedDescription = $derived(
|
||||||
new RegExp(`(${name})`, 'gi'),
|
description.replace(
|
||||||
`<span style="color: ${highlightColor};">$1</span>`
|
new RegExp(`(${name})`, 'gi'),
|
||||||
|
`<span style="color: ${highlightColor};">$1</span>`
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3D tilt effect
|
// 3D tilt effect
|
||||||
let cardElement: HTMLDivElement
|
let cardElement: HTMLDivElement
|
||||||
let isHovering = false
|
let logoElement: HTMLElement
|
||||||
let transform = ''
|
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) {
|
function handleMouseMove(e: MouseEvent) {
|
||||||
if (!cardElement || !isHovering) return
|
if (!cardElement || !isHovering) return
|
||||||
|
|
@ -32,10 +95,31 @@
|
||||||
const centerX = rect.width / 2
|
const centerX = rect.width / 2
|
||||||
const centerY = rect.height / 2
|
const centerY = rect.height / 2
|
||||||
|
|
||||||
const rotateX = ((y - centerY) / centerY) * -4 // -4 to 4 degrees
|
// 3D tilt for card
|
||||||
const rotateY = ((x - centerX) / centerX) * 4 // -4 to 4 degrees
|
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)`
|
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() {
|
function handleMouseEnter() {
|
||||||
|
|
@ -45,6 +129,15 @@
|
||||||
function handleMouseLeave() {
|
function handleMouseLeave() {
|
||||||
isHovering = false
|
isHovering = false
|
||||||
transform = 'perspective(1000px) rotateX(0) rotateY(0) scale3d(1, 1, 1)'
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
@ -56,14 +149,18 @@
|
||||||
on:mouseleave={handleMouseLeave}
|
on:mouseleave={handleMouseLeave}
|
||||||
style="transform: {transform};"
|
style="transform: {transform};"
|
||||||
>
|
>
|
||||||
<div class="project-logo">
|
<div class="project-logo" style="background-color: {backgroundColor}">
|
||||||
<SVGHoverEffect
|
{#if svgContent}
|
||||||
{SVGComponent}
|
<div
|
||||||
{backgroundColor}
|
bind:this={logoElement}
|
||||||
maxMovement={10}
|
class="logo-svg"
|
||||||
containerHeight="80px"
|
style="transform: {logoTransform}"
|
||||||
bounceDamping={0.2}
|
>
|
||||||
/>
|
{@html svgContent}
|
||||||
|
</div>
|
||||||
|
{:else if logoUrl}
|
||||||
|
<img src={logoUrl} alt="{name} logo" class="logo-image" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="project-content">
|
<div class="project-content">
|
||||||
<p class="project-description">{@html highlightedDescription}</p>
|
<p class="project-description">{@html highlightedDescription}</p>
|
||||||
|
|
@ -101,19 +198,31 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 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) {
|
.logo-image {
|
||||||
width: 80px !important;
|
max-width: 100%;
|
||||||
height: 80px !important;
|
max-height: 100%;
|
||||||
border-radius: $unit-2x;
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.15s ease-out;
|
||||||
|
|
||||||
:global(svg) {
|
:global(svg) {
|
||||||
width: 48px !important;
|
width: 48px;
|
||||||
height: 48px !important;
|
height: 48px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,16 +248,6 @@
|
||||||
.project-logo {
|
.project-logo {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 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">
|
<script lang="ts">
|
||||||
import ProjectItem from '$components/ProjectItem.svelte'
|
import ProjectItem from '$components/ProjectItem.svelte'
|
||||||
|
import type { Project } from '$lib/types/project'
|
||||||
|
|
||||||
import MaitsuLogo from '$illos/logo-maitsu.svg?component'
|
interface Props {
|
||||||
import SlackLogo from '$illos/logo-slack.svg?component'
|
projects: Project[]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const projects: Project[] = [
|
let { projects = [] }: Props = $props()
|
||||||
{
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="projects">
|
<section class="projects">
|
||||||
|
|
@ -64,9 +23,21 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
{#if projects.length === 0}
|
||||||
|
<li>
|
||||||
|
<div class="no-projects">No projects found</div>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
{#each projects as project, index}
|
{#each projects as project, index}
|
||||||
<li>
|
<li>
|
||||||
<ProjectItem {...project} {index} />
|
<ProjectItem
|
||||||
|
logoUrl={project.logoUrl}
|
||||||
|
backgroundColor={project.backgroundColor || '#f7f7f7'}
|
||||||
|
name={project.title}
|
||||||
|
description={project.description || ''}
|
||||||
|
highlightColor={project.highlightColor || '#333'}
|
||||||
|
{index}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -114,4 +85,11 @@
|
||||||
color: #d0290d;
|
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">
|
<script lang="ts">
|
||||||
import ProjectList from '$components/ProjectList.svelte'
|
import ProjectList from '$components/ProjectList.svelte'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
|
let { data } = $props<{ data: PageData }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProjectList />
|
<ProjectList projects={data?.projects || []} />
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,34 @@
|
||||||
import type { PageLoad } from './$types'
|
import type { PageLoad } from './$types'
|
||||||
import type { Album } from '$lib/types/lastfm'
|
import type { Album } from '$lib/types/lastfm'
|
||||||
|
import type { Project } from '$lib/types/project'
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
try {
|
try {
|
||||||
// const [albums, steamGames, psnGames] = await Promise.all([
|
// Fetch albums first
|
||||||
const [albums] = await Promise.all([
|
let albums: Album[] = []
|
||||||
fetchRecentAlbums(fetch)
|
try {
|
||||||
// fetchRecentSteamGames(fetch),
|
albums = await fetchRecentAlbums(fetch)
|
||||||
// fetchRecentPSNGames(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 {
|
return {
|
||||||
albums
|
albums,
|
||||||
// games: games,
|
projects: projectsData.projects || []
|
||||||
// steamGames: steamGames,
|
|
||||||
// psnGames: psnGames
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching data:', err)
|
console.error('Error in load function:', err)
|
||||||
return {
|
return {
|
||||||
albums: [],
|
albums: [],
|
||||||
games: [],
|
projects: []
|
||||||
error: err instanceof Error ? err.message : 'An unknown error occurred'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,3 +55,11 @@ async function fetchRecentPSNGames(fetch: typeof window.fetch): Promise<Serializ
|
||||||
}
|
}
|
||||||
return await response.json()
|
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