managarten/picture/apps/landing/src/pages/tutorials/[...slug].astro
Till-JS 36b85fc8a0 fix(picture): migrate to Astro 5.x content collections and fix TypeScript errors
- Rename dynamic routes from [slug].astro to [...slug].astro for multi-segment paths
- Replace deprecated entry.slug with entry.id across all components and utils
- Fix TypeScript implicit any types in TemplateFilters and prompt-templates
- Add proper type narrowing for feature.note in pricing page
- Remove unused marked import from FAQCard
- Delete invalid placeholder content files
- Add shared-landing-ui dependency and integrate StepsSection/PricingSection
- Update tailwind config with shared-landing-ui content paths
- Add global.css with Indigo/Violet dark theme variables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 18:13:57 +01:00

305 lines
10 KiB
Text

---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import TutorialCard from '../../components/tutorials/TutorialCard.astro';
import StepIndicator from '../../components/tutorials/StepIndicator.astro';
import {
getDifficultyDisplayName,
getDifficultyIcon,
getDifficultyColor,
getCategoryDisplayName,
getCategoryIcon,
getRelatedTutorials,
} from '../../utils/tutorials';
export async function getStaticPaths() {
const tutorials = await getCollection('tutorials');
return tutorials.map((tutorial) => ({
params: { slug: tutorial.id },
props: { tutorial },
}));
}
const { tutorial } = Astro.props;
const { Content } = await tutorial.render();
const relatedTutorials = await getRelatedTutorials(tutorial, 3);
const { data } = tutorial;
const difficultyColor = getDifficultyColor(data.difficulty);
---
<Layout title={`${data.title} - Tutorial | Picture`} description={data.description}>
<div class="bg-dark-bg min-h-screen">
<!-- Breadcrumbs -->
<div class="max-w-4xl mx-auto px-6 pt-8">
<nav class="flex items-center gap-2 text-sm text-gray-400">
<a href="/" class="hover:text-primary">Home</a>
<span>/</span>
<a href="/tutorials" class="hover:text-primary">Tutorials</a>
<span>/</span>
<a href={`/tutorials?category=${data.category}`} class="hover:text-primary">
{getCategoryDisplayName(data.category)}
</a>
<span>/</span>
<span class="text-white">{data.title}</span>
</nav>
</div>
<!-- Hero -->
<div class="max-w-4xl mx-auto px-6 py-12">
<div class="flex items-center gap-3 mb-6 flex-wrap">
<span class="text-3xl">{data.icon}</span>
<span
class="px-3 py-1 bg-dark-elevated border border-dark-border rounded-lg text-sm text-gray-300"
>
{getCategoryIcon(data.category)} {getCategoryDisplayName(data.category)}
</span>
<span
class={`px-3 py-1 bg-dark-elevated border border-dark-border rounded-lg text-sm ${difficultyColor}`}
>
{getDifficultyIcon(data.difficulty)} {getDifficultyDisplayName(data.difficulty)}
</span>
{data.hasVideo && (
<span
class="px-3 py-1 bg-dark-elevated border border-dark-border rounded-lg text-sm text-gray-300"
>
🎥 Video included
</span>
)}
</div>
<h1 class="text-4xl md:text-5xl font-bold text-white mb-6">{data.title}</h1>
<p class="text-xl text-gray-300 mb-8">{data.description}</p>
<!-- Meta Info -->
<div class="flex items-center gap-6 text-sm text-gray-400 flex-wrap mb-8">
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>{data.estimatedTime}</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
></path>
</svg>
<span>{data.steps.length} steps</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
></path>
</svg>
<span>Updated {data.lastUpdated.toLocaleDateString()}</span>
</div>
</div>
<!-- What you'll learn -->
{data.whatYouWillLearn.length > 0 && (
<div class="bg-dark-elevated border border-dark-border rounded-xl p-6 mb-8">
<h2 class="text-lg font-semibold text-white mb-4">What you'll learn:</h2>
<ul class="space-y-2">
{data.whatYouWillLearn.map((item) => (
<li class="flex items-start gap-3 text-gray-300">
<svg class="w-5 h-5 text-primary mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
></path>
</svg>
<span>{item}</span>
</li>
))}
</ul>
</div>
)}
<!-- Prerequisites -->
{data.prerequisites.length > 0 && (
<div class="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-6 mb-8">
<h3 class="text-lg font-semibold text-yellow-400 mb-3 flex items-center gap-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"></path>
</svg>
Prerequisites
</h3>
<ul class="space-y-1">
{data.prerequisites.map((item) => (
<li class="text-yellow-100">• {item}</li>
))}
</ul>
</div>
)}
<!-- Video -->
{data.hasVideo && data.videoUrl && (
<div class="mb-12">
<div class="aspect-video bg-dark-elevated rounded-xl overflow-hidden border border-dark-border">
<iframe
src={data.videoUrl}
class="w-full h-full"
allowfullscreen
title={data.title}
></iframe>
</div>
</div>
)}
</div>
<!-- Step Indicator (Sticky) -->
<StepIndicator steps={data.steps} />
<!-- Tutorial Content -->
<div class="max-w-4xl mx-auto px-6 pb-12">
<article
class="prose prose-invert prose-lg max-w-none
prose-headings:text-white prose-headings:font-bold
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
prose-p:text-gray-300 prose-p:leading-relaxed
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-strong:text-white prose-strong:font-semibold
prose-code:text-primary prose-code:bg-dark-elevated prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded
prose-pre:bg-dark-elevated prose-pre:border prose-pre:border-dark-border
prose-ul:text-gray-300 prose-ol:text-gray-300
prose-li:marker:text-primary
prose-blockquote:border-l-primary prose-blockquote:bg-dark-elevated prose-blockquote:py-1"
>
<Content />
</article>
<!-- Tips -->
{data.tips.length > 0 && (
<div class="mt-12 bg-primary/10 border border-primary/20 rounded-xl p-6">
<h3 class="text-xl font-semibold text-primary mb-4 flex items-center gap-2">
<span>💡</span> Pro Tips
</h3>
<ul class="space-y-2">
{data.tips.map((tip) => (
<li class="text-gray-300">• {tip}</li>
))}
</ul>
</div>
)}
<!-- Common Mistakes -->
{data.commonMistakes.length > 0 && (
<div class="mt-8 bg-red-500/10 border border-red-500/20 rounded-xl p-6">
<h3 class="text-xl font-semibold text-red-400 mb-4 flex items-center gap-2">
<span>⚠️</span> Common Mistakes to Avoid
</h3>
<ul class="space-y-2">
{data.commonMistakes.map((mistake) => (
<li class="text-gray-300">• {mistake}</li>
))}
</ul>
</div>
)}
<!-- Troubleshooting -->
{data.troubleshooting.length > 0 && (
<div class="mt-8 bg-dark-elevated border border-dark-border rounded-xl p-6">
<h3 class="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<span>🔧</span> Troubleshooting
</h3>
<div class="space-y-4">
{data.troubleshooting.map(({ problem, solution }) => (
<div>
<p class="font-medium text-yellow-400 mb-1">Problem: {problem}</p>
<p class="text-gray-300">Solution: {solution}</p>
</div>
))}
</div>
</div>
)}
<!-- Downloadable Resources -->
{data.downloadableResources.length > 0 && (
<div class="mt-12 bg-dark-elevated border border-dark-border rounded-xl p-6">
<h3 class="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<span>📥</span> Download Resources
</h3>
<div class="space-y-3">
{data.downloadableResources.map((resource) => (
<a
href={resource.url}
class="flex items-center justify-between p-4 bg-dark-bg border border-dark-border rounded-lg hover:border-primary transition group"
>
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center text-primary">
{resource.type === 'template' && '📄'}
{resource.type === 'preset' && '⚙️'}
{resource.type === 'example' && '🎨'}
{resource.type === 'cheatsheet' && '📋'}
</div>
<div>
<p class="text-white font-medium">{resource.title}</p>
<p class="text-sm text-gray-400 capitalize">{resource.type}</p>
</div>
</div>
<svg
class="w-5 h-5 text-gray-400 group-hover:text-primary transition"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
></path>
</svg>
</a>
))}
</div>
</div>
)}
<!-- CTA -->
<div class="mt-12 bg-gradient-to-r from-primary/10 to-purple-500/10 border border-primary/20 rounded-xl p-8 text-center">
<h3 class="text-2xl font-bold text-white mb-3">Ready to try it yourself?</h3>
<p class="text-gray-300 mb-6">
Put what you learned into practice with Picture's AI image generator.
</p>
<a
href="#"
class="px-8 py-3 bg-primary text-white font-medium rounded-lg inline-block hover:bg-primary/90 transition"
>
Start Creating
</a>
</div>
</div>
<!-- Related Tutorials -->
{relatedTutorials.length > 0 && (
<div class="bg-dark-elevated py-16">
<div class="max-w-6xl mx-auto px-6">
<h2 class="text-2xl font-bold text-white mb-8">Related Tutorials</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{relatedTutorials.map((relatedTutorial) => (
<TutorialCard tutorial={relatedTutorial} />
))}
</div>
</div>
</div>
)}
</div>
</Layout>