Refactor project page a bit, consistent arrow icons
This commit is contained in:
parent
8d68219dde
commit
42a84f8ad7
7 changed files with 246 additions and 224 deletions
4
src/assets/icons/arrow-left.svg
Normal file
4
src/assets/icons/arrow-left.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33337 8H12.6667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.66663 3.33325L2.99996 7.99992L7.66663 12.6666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
4
src/assets/icons/arrow-right.svg
Normal file
4
src/assets/icons/arrow-right.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33337 8H12.6667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 3.33325L12.6667 7.99992L8 12.6666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 277 B |
|
|
@ -4,6 +4,7 @@
|
|||
import { formatDate } from '$lib/utils/date'
|
||||
import { renderEdraContent } from '$lib/utils/content'
|
||||
import { goto } from '$app/navigation'
|
||||
import ArrowLeft from '$icons/arrow-left.svg'
|
||||
|
||||
let { post }: { post: any } = $props()
|
||||
|
||||
|
|
@ -82,15 +83,7 @@
|
|||
|
||||
<footer class="post-footer">
|
||||
<button onclick={() => goto('/universe')} class="back-button">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="back-arrow">
|
||||
<path
|
||||
d="M15 8H3.5M3.5 8L8 3.5M3.5 8L8 12.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.25"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowLeft class="back-arrow" />
|
||||
Back to Universe
|
||||
</button>
|
||||
</footer>
|
||||
|
|
@ -345,20 +338,28 @@
|
|||
border-radius: 24px;
|
||||
outline: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
.back-arrow {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
&:hover:not(:disabled) :global(.back-arrow) {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba($red-60, 0.25);
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
:global(.back-arrow) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: -$unit-half;
|
||||
|
||||
:global(path) {
|
||||
stroke: currentColor;
|
||||
stroke-width: 2.25;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Project } from '$lib/types/project'
|
||||
import ArrowLeft from '$icons/arrow-left.svg'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
|
|
@ -48,45 +50,6 @@
|
|||
</script>
|
||||
|
||||
<article class="project-content">
|
||||
<!-- Project Details -->
|
||||
<div class="project-details">
|
||||
<div class="meta-grid">
|
||||
{#if project.client}
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Client</span>
|
||||
<span class="meta-value">{project.client}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if project.year}
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Year</span>
|
||||
<span class="meta-value">{project.year}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if project.role}
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Role</span>
|
||||
<span class="meta-value">{project.role}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if project.externalUrl}
|
||||
<div class="external-link-wrapper">
|
||||
<a
|
||||
href={project.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="external-link"
|
||||
>
|
||||
Visit Project →
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Case Study Content -->
|
||||
{#if project.caseStudyContent && project.caseStudyContent.content && project.caseStudyContent.content.length > 0}
|
||||
<div class="case-study-section">
|
||||
|
|
@ -109,13 +72,19 @@
|
|||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="project-nav">
|
||||
<footer class="project-footer">
|
||||
{#if project.projectType === 'labs'}
|
||||
<a href="/labs" class="back-link">← Back to labs</a>
|
||||
<button onclick={() => goto('/labs')} class="back-button">
|
||||
<ArrowLeft class="back-arrow" />
|
||||
Back to labs
|
||||
</button>
|
||||
{:else}
|
||||
<a href="/" class="back-link">← Back to projects</a>
|
||||
<button onclick={() => goto('/')} class="back-button">
|
||||
<ArrowLeft class="back-arrow" />
|
||||
Back to projects
|
||||
</button>
|
||||
{/if}
|
||||
</nav>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -126,58 +95,6 @@
|
|||
gap: $unit-4x;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-3x;
|
||||
padding-bottom: $unit-3x;
|
||||
border-bottom: 1px solid $grey-90;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: $unit-2x;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 0.875rem;
|
||||
color: $grey-60;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 1rem;
|
||||
color: $grey-20;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.external-link-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.external-link {
|
||||
display: inline-block;
|
||||
padding: $unit-2x $unit-3x;
|
||||
background: $grey-10;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
font-weight: 500;
|
||||
font-size: 0.925rem;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: $grey-20;
|
||||
}
|
||||
}
|
||||
|
||||
/* Case Study Section */
|
||||
.case-study-content {
|
||||
|
|
@ -238,9 +155,6 @@
|
|||
|
||||
/* Gallery Section */
|
||||
.gallery-section {
|
||||
padding-top: $unit-3x;
|
||||
border-top: 1px solid $grey-90;
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
margin: 0 0 $unit-3x;
|
||||
|
|
@ -262,20 +176,47 @@
|
|||
}
|
||||
|
||||
/* Navigation */
|
||||
.project-nav {
|
||||
text-align: center;
|
||||
padding-top: $unit-3x;
|
||||
border-top: 1px solid $grey-90;
|
||||
.project-footer {
|
||||
padding-bottom: $unit-2x;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: $grey-40;
|
||||
text-decoration: none;
|
||||
font-size: 0.925rem;
|
||||
transition: color 0.2s ease;
|
||||
.back-button {
|
||||
color: $red-60;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
border-radius: 24px;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: $grey-20;
|
||||
&:hover:not(:disabled) :global(.back-arrow) {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba($red-60, 0.25);
|
||||
}
|
||||
|
||||
:global(.back-arrow) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: -$unit-half;
|
||||
|
||||
:global(path) {
|
||||
stroke: currentColor;
|
||||
stroke-width: 2.25;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
113
src/lib/components/ProjectHeaderContent.svelte
Normal file
113
src/lib/components/ProjectHeaderContent.svelte
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts">
|
||||
import ArrowRight from '$icons/arrow-right.svg'
|
||||
import type { Project } from '$lib/types/project'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
}
|
||||
|
||||
let { project }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="project-header-content">
|
||||
<div class="project-text">
|
||||
<h1 class="project-title">{project.title}</h1>
|
||||
</div>
|
||||
{#if project.externalUrl}
|
||||
<a
|
||||
href={project.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="visit-button"
|
||||
style="--button-bg: {project.highlightColor || '#4d4d4d'}; --button-color: white"
|
||||
>
|
||||
Visit <ArrowRight />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
/* Project Header */
|
||||
.project-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: $unit-3x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.project-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 2.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 $unit;
|
||||
color: $grey-10;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.project-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: $grey-40;
|
||||
margin: 0;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.visit-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
padding: ($unit * 1.5) $unit-2x;
|
||||
background: var(--button-bg, $grey-10);
|
||||
color: var(--button-color, white);
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
:global(svg) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
:global(path) {
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--button-bg) 90%, black);
|
||||
|
||||
:global(svg) {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import DynamicPostContent from '$components/DynamicPostContent.svelte'
|
||||
import { getContentExcerpt } from '$lib/utils/content'
|
||||
import { goto } from '$app/navigation'
|
||||
import ArrowLeft from '$icons/arrow-left.svg'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
|
@ -45,15 +46,7 @@
|
|||
<h1>Post Not Found</h1>
|
||||
<p>{error || "The post you're looking for doesn't exist."}</p>
|
||||
<button onclick={() => goto('/universe')} class="back-button">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="back-arrow">
|
||||
<path
|
||||
d="M15 8H3.5M3.5 8L8 3.5M3.5 8L8 12.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.25"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowLeft class="back-arrow" />
|
||||
Back to Universe
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -106,20 +99,28 @@
|
|||
border-radius: 24px;
|
||||
outline: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
.back-arrow {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
&:hover:not(:disabled) :global(.back-arrow) {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba($red-60, 0.25);
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
:global(.back-arrow) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: -$unit-half;
|
||||
|
||||
:global(path) {
|
||||
stroke: currentColor;
|
||||
stroke-width: 2.25;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import Page from '$components/Page.svelte'
|
||||
import ProjectPasswordProtection from '$lib/components/ProjectPasswordProtection.svelte'
|
||||
import ProjectHeaderContent from '$lib/components/ProjectHeaderContent.svelte'
|
||||
import ProjectContent from '$lib/components/ProjectContent.svelte'
|
||||
import type { PageData } from './$types'
|
||||
import type { Project } from '$lib/types/project'
|
||||
|
|
@ -12,14 +13,14 @@
|
|||
const error = $derived(data.error as string | undefined)
|
||||
|
||||
let headerContainer = $state<HTMLElement | null>(null)
|
||||
|
||||
|
||||
// Spring with aggressive bounce settings
|
||||
const logoPosition = spring(
|
||||
{ x: 0, y: 0 },
|
||||
{
|
||||
stiffness: 0.03, // Extremely low for maximum bounce
|
||||
damping: 0.1, // Very low for many oscillations
|
||||
precision: 0.001 // Keep animating for longer
|
||||
stiffness: 0.03, // Extremely low for maximum bounce
|
||||
damping: 0.1, // Very low for many oscillations
|
||||
precision: 0.001 // Keep animating for longer
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -72,70 +73,49 @@
|
|||
<a href="/" class="back-link">← Back to projects</a>
|
||||
</div>
|
||||
</Page>
|
||||
{:else if project.status === 'password-protected'}
|
||||
<div class="project-wrapper">
|
||||
<div
|
||||
bind:this={headerContainer}
|
||||
class="project-header-container"
|
||||
style="background-color: {project.backgroundColor || '#f5f5f5'}"
|
||||
onmousemove={handleMouseMove}
|
||||
onmouseleave={handleMouseLeave}
|
||||
>
|
||||
{#if project.logoUrl}
|
||||
<img
|
||||
src={project.logoUrl}
|
||||
alt="{project.title} logo"
|
||||
class="project-logo"
|
||||
style="transform: {logoTransform}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<Page>
|
||||
<ProjectPasswordProtection
|
||||
projectSlug={project.slug}
|
||||
correctPassword={project.password || ''}
|
||||
projectType="work"
|
||||
{:else if project.status === 'password-protected' || project.status === 'published'}
|
||||
{#snippet projectLayout()}
|
||||
<div class="project-wrapper">
|
||||
<div
|
||||
bind:this={headerContainer}
|
||||
class="project-header-container"
|
||||
style="background-color: {project.backgroundColor || '#f5f5f5'}"
|
||||
onmousemove={handleMouseMove}
|
||||
onmouseleave={handleMouseLeave}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#snippet children()}
|
||||
<div slot="header" class="project-header">
|
||||
<h1 class="project-title">{project.title}</h1>
|
||||
{#if project.subtitle}
|
||||
<p class="project-subtitle">{project.subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<ProjectContent {project} />
|
||||
{/snippet}
|
||||
</ProjectPasswordProtection>
|
||||
</Page>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="project-wrapper">
|
||||
<div
|
||||
bind:this={headerContainer}
|
||||
class="project-header-container"
|
||||
style="background-color: {project.backgroundColor || '#f5f5f5'}"
|
||||
onmousemove={handleMouseMove}
|
||||
onmouseleave={handleMouseLeave}
|
||||
>
|
||||
{#if project.logoUrl}
|
||||
<img
|
||||
src={project.logoUrl}
|
||||
alt="{project.title} logo"
|
||||
class="project-logo"
|
||||
style="transform: {logoTransform}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<Page>
|
||||
<div slot="header" class="project-header">
|
||||
<h1 class="project-title">{project.title}</h1>
|
||||
{#if project.subtitle}
|
||||
<p class="project-subtitle">{project.subtitle}</p>
|
||||
{#if project.logoUrl}
|
||||
<img
|
||||
src={project.logoUrl}
|
||||
alt="{project.title} logo"
|
||||
class="project-logo"
|
||||
style="transform: {logoTransform}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<ProjectContent {project} />
|
||||
</Page>
|
||||
</div>
|
||||
<Page>
|
||||
<div slot="header" class="project-header">
|
||||
<ProjectHeaderContent {project} />
|
||||
</div>
|
||||
{#if project.status === 'password-protected'}
|
||||
<ProjectPasswordProtection
|
||||
projectSlug={project.slug}
|
||||
correctPassword={project.password || ''}
|
||||
projectType="work"
|
||||
>
|
||||
{#snippet children()}
|
||||
<ProjectContent {project} />
|
||||
{/snippet}
|
||||
</ProjectPasswordProtection>
|
||||
{:else}
|
||||
<ProjectContent {project} />
|
||||
{/if}
|
||||
</Page>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{@render projectLayout()}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -230,28 +210,6 @@
|
|||
|
||||
/* Project Header */
|
||||
.project-header {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 $unit;
|
||||
color: $grey-10;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.project-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: $grey-40;
|
||||
margin: 0;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue