Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/SEO.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const {
type = 'website',
} = Astro.props;

const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const ogImage = new URL(image, Astro.site);
const siteBase = Astro.site ?? Astro.url.origin;
const canonicalURL = new URL(Astro.url.pathname, siteBase);
const ogImage = new URL(image, siteBase);
const fullTitle = title === 'Spicetify' ? title : `${title} | Spicetify`;
---

Expand Down
174 changes: 174 additions & 0 deletions src/components/homepage/FeaturesGrid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
import ArrowIcon from '../icons/ArrowIcon.astro';

interface Props {
features: {
icon: string;
title: string;
description: string;
href: string;
}[];
}

const { features } = Astro.props;
---

<section id="features" class="section features-section" aria-labelledby="features-heading">
<div class="section-container">
<div class="section-header">
<span class="section-tag">Features</span>
<h2 id="features-heading">Everything you need</h2>
<p>A complete toolkit to customize every corner of Spotify.</p>
</div>

<div class="features-grid">
{features.map((feature) => (
<a href={feature.href} class="feature-card">
<div class="feature-icon" data-icon={feature.icon}>
{feature.icon === 'palette' && (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="13.5" cy="6.5" r="2.5" /><circle cx="17.5" cy="10.5" r="2.5" /><circle cx="8.5" cy="7.5" r="2.5" /><circle cx="6.5" cy="12.5" r="2.5" />
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" />
</svg>
)}
{feature.icon === 'puzzle' && (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 2v4m0 12v4M2 12h4m12 0h4m-3.17-6.83-2.83 2.83m-4 4-2.83 2.83M6.83 5.17l2.83 2.83m4 4 2.83 2.83" />
</svg>
)}
{feature.icon === 'grid' && (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
)}
{feature.icon === 'terminal' && (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" />
</svg>
)}
</div>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
<span class="feature-arrow">
<ArrowIcon size={14} />
</span>
</a>
))}
</div>
</div>
</section>

<style>
.features-section {
background: var(--color-bg-secondary);
}

.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}

.feature-card {
position: relative;
display: flex;
flex-direction: column;
padding: 1.75rem;
border-radius: 14px;
border: 1px solid var(--color-border);
background: var(--color-bg);
text-decoration: none;
color: var(--color-text);
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
overflow: hidden;
}

.feature-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, var(--accent-subtle) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}

.feature-card:hover {
text-decoration: none;
transform: translateY(-2px);
border-color: var(--accent-border-hover);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.06);
}

.feature-card:hover::before {
opacity: 1;
}

.feature-card:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

.feature-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 10px;
background: var(--accent-soft);
color: var(--accent);
margin-bottom: 1rem;
}

.feature-card h3 {
position: relative;
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.4rem;
}

.feature-card p {
position: relative;
font-size: 0.88rem;
color: var(--color-text-secondary);
line-height: 1.55;
margin: 0;
}

.feature-arrow {
position: absolute;
top: 1.75rem;
right: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--accent-soft);
color: var(--accent);
opacity: 0;
transform: translateX(-4px);
transition: opacity 0.25s ease, transform 0.25s ease;
}

.feature-card:hover .feature-arrow {
opacity: 1;
transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
.feature-card,
.feature-arrow {
transition: none !important;
}
}

@media (max-width: 900px) {
.features-grid {
grid-template-columns: 1fr;
}
}
</style>
Loading