diff --git a/astro.config.mjs b/astro.config.mjs index e762ba5..d356219 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,5 +1,19 @@ // @ts-check import { defineConfig } from 'astro/config'; +import tailwindcss from '@tailwindcss/vite'; +import sitemap from '@astrojs/sitemap'; -// https://astro.build/config -export default defineConfig({}); +export default defineConfig({ + site: 'https://dusa.cz', + vite: { + plugins: [tailwindcss()], + }, + integrations: [sitemap()], + i18n: { + defaultLocale: 'cs', + locales: ['cs', 'en'], + routing: { + prefixDefaultLocale: true, + }, + }, +}); \ No newline at end of file diff --git a/package.json b/package.json index 9831250..0a5760c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,11 @@ "astro": "astro" }, "dependencies": { - "astro": "^5.17.1" + "@astrojs/sitemap": "^3.7.0", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.2.1", + "astro": "^5.17.1", + "fuse.js": "^7.1.0", + "tailwindcss": "^4.2.1" } -} \ No newline at end of file +} diff --git a/public/images/blog/placeholder1.jpg b/public/images/blog/placeholder1.jpg new file mode 100644 index 0000000..3813661 Binary files /dev/null and b/public/images/blog/placeholder1.jpg differ diff --git a/public/images/blog/placeholder2.jpg b/public/images/blog/placeholder2.jpg new file mode 100644 index 0000000..aff1ea0 Binary files /dev/null and b/public/images/blog/placeholder2.jpg differ diff --git a/public/images/blog/placeholder3.jpg b/public/images/blog/placeholder3.jpg new file mode 100644 index 0000000..48f8a5f Binary files /dev/null and b/public/images/blog/placeholder3.jpg differ diff --git a/public/images/blog/placeholder4.jpg b/public/images/blog/placeholder4.jpg new file mode 100644 index 0000000..cb39e5b Binary files /dev/null and b/public/images/blog/placeholder4.jpg differ diff --git a/public/images/blog/placeholder5.jpg b/public/images/blog/placeholder5.jpg new file mode 100644 index 0000000..065f498 Binary files /dev/null and b/public/images/blog/placeholder5.jpg differ diff --git a/public/images/blog/placeholder6.jpg b/public/images/blog/placeholder6.jpg new file mode 100644 index 0000000..f373cfa Binary files /dev/null and b/public/images/blog/placeholder6.jpg differ diff --git a/src/components/ArticleTile.astro b/src/components/ArticleTile.astro new file mode 100644 index 0000000..bed5d22 --- /dev/null +++ b/src/components/ArticleTile.astro @@ -0,0 +1,28 @@ +--- +interface Props { + title: string; + href: string; + image: string; + description?: string; + class?: string; + eager?: boolean; + tags?: string[]; +} + +const { title, href, image, description, class: className = '', eager = false, tags = [] } = Astro.props; +--- + + + {title} +
+ {tags.length > 0 && ( +
+ {tags.map((tag, i) => ( + {tag}{i < tags.length - 1 && · } + ))} +
+ )} +

{title}

+ {description &&

{description}

} +
+
diff --git a/src/content/blog/cs/ai-a-vzdelavani.md b/src/content/blog/cs/ai-a-vzdelavani.md new file mode 100644 index 0000000..bee478a --- /dev/null +++ b/src/content/blog/cs/ai-a-vzdelavani.md @@ -0,0 +1,34 @@ +--- +title: Jak AI mění způsob, jakým se učíme +date: 2024-11-15 +description: Umělá inteligence proniká do škol i domácností. Co to znamená pro budoucnost vzdělávání a roli učitele? +image: /images/blog/placeholder1.jpg +draft: false +tags: [AI, vzdělávání] +--- + +Ještě před pěti lety bylo personalizované učení s pomocí umělé inteligence doménou sci-fi románů. Dnes si žáci základních škol nechávají vysvětlovat matematiku od chatbotů a studenti vysokých škol používají AI jako prvního rádce při psaní seminárních prací. + +## Tradiční model vzdělávání pod tlakem + +Tradiční model vzdělávání stál na předpokladu, že učitel je hlavním zdrojem poznání. Tento model fungoval staletí — a funguje dodnes. AI ho neboří, ale výrazně přetváří. Znalosti jsou nyní dostupné okamžitě, kdykoli a kýmkoli. Otázka přestává být *co se naučit* a stává se *jak se to naučit* a *proč*. + +## Co AI ve vzdělávání umí dnes + +Personalizované učení přizpůsobené tempu každého žáka bylo vždy snem pedagogů. Dnes je to technicky možné. Nástroje jako Khan Academy nebo Duolingo využívají AI k tomu, aby sledovaly pokrok, identifikovaly mezery ve znalostech a přizpůsobovaly obsah v reálném čase. + +Vedle toho AI pomáhá s: + +- **Okamžitou zpětnou vazbou** — žák nemusí čekat na opravenou písemku +- **Překladem a jazykovou asistencí** — přístup ke znalostem bez jazykové bariéry +- **Generováním vysvětlení** — složitý pojem lze vysvětlit deseti různými způsoby, dokud jeden nezapadne + +## Největší riziko: myšlení na autopilotu + +Největším rizikem není, že AI nahradí učitele. Největším rizikem je, že přestaneme rozvíjet schopnost hluboce přemýšlet, protože odpověď je vždy na dosah ruky. Když nemusíme zápasit s problémem, nevznikají ty nejcennější spoje v mozku. + +Dobrý učitel to ví odjakživa — nechá žáka chvíli bojovat, než zasáhne. AI zatím tuto citlivost postrádá. + +## Role učitele se mění, ne mizí + +Učitel budoucnosti bude méně přenašečem informací a více průvodcem. Bude pomáhat žákům orientovat se v záplavě dat, kriticky hodnotit zdroje a nacházet vlastní motivaci k učení. To jsou schopnosti, které AI doplní, ale nenahradí. diff --git a/src/content/blog/cs/ctvrta.md b/src/content/blog/cs/ctvrta.md new file mode 100644 index 0000000..7122e21 --- /dev/null +++ b/src/content/blog/cs/ctvrta.md @@ -0,0 +1,14 @@ +--- +title: Čtvrtý článek +date: 2023-12-10 +description: Čtvrtý ukázkový článek s homeschooling tematikou. +image: /images/blog/placeholder4.jpg +draft: false +tags: [vzdělávání] +--- + +Sem přijde text článku. Nějaké Slovo. + +## Podnadpis + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/src/content/blog/cs/druha.md b/src/content/blog/cs/druha.md new file mode 100644 index 0000000..de22835 --- /dev/null +++ b/src/content/blog/cs/druha.md @@ -0,0 +1,14 @@ +--- +title: Druhý článek +date: 2024-02-15 +description: Toto je druhý článek na webu. +image: /images/blog/placeholder2.jpg +draft: false +tags: [vzdělávání] +--- + +Sem přijde text druhého článku. + +## Podnadpis + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/src/content/blog/cs/pata.md b/src/content/blog/cs/pata.md new file mode 100644 index 0000000..7e6c56c --- /dev/null +++ b/src/content/blog/cs/pata.md @@ -0,0 +1,14 @@ +--- +title: Pátý článek +date: 2023-11-05 +description: Pátý ukázkový článek. +image: /images/blog/placeholder5.jpg +draft: false +tags: [AI, názor] +--- + +Sem přijde text pátého článku. + +## Podnadpis + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/src/content/blog/cs/sesta.md b/src/content/blog/cs/sesta.md new file mode 100644 index 0000000..15b206c --- /dev/null +++ b/src/content/blog/cs/sesta.md @@ -0,0 +1,14 @@ +--- +title: Šestý článek +date: 2023-10-01 +description: Šestý ukázkový článek. +image: /images/blog/placeholder6.jpg +draft: false +tags: [vzdělávání] +--- + +Sem přijde text šestého článku. + +## Podnadpis + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/src/content/blog/cs/treti.md b/src/content/blog/cs/treti.md new file mode 100644 index 0000000..8c7fa5a --- /dev/null +++ b/src/content/blog/cs/treti.md @@ -0,0 +1,14 @@ +--- +title: Třetí článek +date: 2024-01-20 +description: Třetí ukázkový článek. +image: /images/blog/placeholder3.jpg +draft: false +tags: [názor] +--- + +Sem přijde text třetího článku. + +## Podnadpis + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/src/content/blog/cs/ukazka.md b/src/content/blog/cs/ukazka.md new file mode 100644 index 0000000..163f706 --- /dev/null +++ b/src/content/blog/cs/ukazka.md @@ -0,0 +1,14 @@ +--- +title: Ukázkový článek +date: 2024-03-01 +description: Toto je ukázkový článek pro testování vzhledu kachlí na webu. +image: /images/blog/placeholder1.jpg +draft: false +tags: [AI, Technologie] +--- + +Sem přijde text článku. Článek může obsahovat libovolný Markdown obsah. + +## Podnadpis + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. diff --git a/src/content/blog/en/ai-a-vzdelavani.md b/src/content/blog/en/ai-a-vzdelavani.md new file mode 100644 index 0000000..0865324 --- /dev/null +++ b/src/content/blog/en/ai-a-vzdelavani.md @@ -0,0 +1,34 @@ +--- +title: How AI Is Changing the Way We Learn +date: 2024-11-15 +description: Artificial intelligence is making its way into schools and homes alike. What does this mean for the future of education and the role of teachers? +image: /images/blog/placeholder1.jpg +draft: false +tags: [AI, Education] +--- + +Just five years ago, personalised learning powered by artificial intelligence was the stuff of science fiction. Today, primary school pupils have maths explained to them by chatbots, and university students treat AI as their first port of call when writing essays. + +## Traditional Education Under Pressure + +The traditional model of education rested on the assumption that the teacher is the primary source of knowledge. This model worked for centuries — and it still does. AI doesn't destroy it, but it significantly reshapes it. Knowledge is now available instantly, anytime, to anyone. The question is no longer *what to learn* but *how to learn it* and *why*. + +## What AI Can Do in Education Today + +Personalised learning tailored to each student's pace has always been a dream for educators. Today it's technically possible. Tools like Khan Academy or Duolingo use AI to track progress, identify knowledge gaps, and adapt content in real time. + +Beyond that, AI helps with: + +- **Instant feedback** — students don't have to wait for a marked test +- **Translation and language support** — access to knowledge without language barriers +- **Generating explanations** — a complex concept can be explained ten different ways until one clicks + +## The Biggest Risk: Thinking on Autopilot + +The biggest risk isn't that AI will replace teachers. The biggest risk is that we'll stop developing the ability to think deeply, because the answer is always within reach. When we don't have to wrestle with a problem, the most valuable neural connections never form. + +Good teachers have always known this — they let a student struggle for a while before stepping in. AI still lacks this sensitivity. + +## The Teacher's Role Is Changing, Not Disappearing + +The teacher of the future will be less of an information transmitter and more of a guide. They will help students navigate the flood of data, critically evaluate sources, and find their own motivation to learn. These are abilities that AI will complement, but never replace. diff --git a/src/content/blog/en/ukazka.md b/src/content/blog/en/ukazka.md new file mode 100644 index 0000000..3f0bee4 --- /dev/null +++ b/src/content/blog/en/ukazka.md @@ -0,0 +1,14 @@ +--- +title: Sample Article +date: 2024-03-01 +description: This is a sample article for testing the tile appearance on the web. +image: /images/blog/placeholder.jpg +draft: false +tags: [AI, Technology] +--- + +This is the article text. Articles can contain any Markdown content. + +## Subheading + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..dc440e4 --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,15 @@ +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + type: 'content', + schema: z.object({ + title: z.string(), + date: z.date(), + description: z.string(), + image: z.string(), + draft: z.boolean().default(false), + tags: z.array(z.string()).default([]), + }), +}); + +export const collections = { blog }; diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts new file mode 100644 index 0000000..a4d1ecc --- /dev/null +++ b/src/i18n/ui.ts @@ -0,0 +1,49 @@ +export const languages = { cs: 'CS', en: 'EN' }; +export const defaultLang = 'cs' as const; + +export const ui = { + cs: { + 'nav.articles': 'Články', + 'nav.bio': 'Bio', + 'nav.search': 'Hledat', + 'filter.all': 'Vše', + 'article.back': '← Všechny články', + 'article.no-translation': 'Tento článek není dostupný v angličtině. Zobrazujeme českou verzi.', + 'search.placeholder': 'Hledat v článcích…', + 'search.empty': 'Žádné výsledky.', + 'search.results.one': 'výsledek', + 'search.results.few': 'výsledky', + 'search.results.many': 'výsledků', + 'empty.articles': 'Zatím žádné články.', + }, + en: { + 'nav.articles': 'Articles', + 'nav.bio': 'Bio', + 'nav.search': 'Search', + 'filter.all': 'All', + 'article.back': '← All articles', + 'article.no-translation': 'This article is not available in English. Showing the Czech version.', + 'search.placeholder': 'Search articles…', + 'search.empty': 'No results.', + 'search.results.one': 'result', + 'search.results.few': 'results', + 'search.results.many': 'results', + 'empty.articles': 'No articles yet.', + }, +} as const; + +export type Lang = keyof typeof ui; + +export function useTranslations(lang: Lang) { + return function t(key: keyof typeof ui[typeof defaultLang]) { + return (ui[lang] as Record)[key] ?? (ui[defaultLang] as Record)[key]; + }; +} + +export function getArticlesPath(lang: Lang) { + return lang === 'cs' ? `/${lang}/clanky` : `/${lang}/articles`; +} + +export function getSearchPath(lang: Lang) { + return lang === 'cs' ? `/${lang}/hledat` : `/${lang}/search`; +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..d8b1469 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,81 @@ +--- +import '../styles/global.css'; +import { useTranslations, getArticlesPath, getSearchPath } from '../i18n/ui'; +import type { Lang } from '../i18n/ui'; + +interface Props { + title: string; + description?: string; + lang: Lang; + alternateUrl?: string; +} + +const { title, description = 'Osobní web Vladimíra Duši', lang, alternateUrl } = Astro.props; +const canonicalURL = new URL(Astro.url.pathname, Astro.site); +const t = useTranslations(lang); + +const csUrl = lang === 'cs' ? Astro.url.pathname : (alternateUrl ?? '/cs/'); +const enUrl = lang === 'en' ? Astro.url.pathname : (alternateUrl ?? '/en/'); +--- + + + + + + + + + + + + {title === 'Vladimír Duša' ? title : `${title} — Vladimír Duša`} + + + +
+ + Vladimír Duša + + +
+
+ +
+ + + diff --git a/src/pages/cs/bio.astro b/src/pages/cs/bio.astro new file mode 100644 index 0000000..1e44a2e --- /dev/null +++ b/src/pages/cs/bio.astro @@ -0,0 +1,23 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; + +const lang = 'cs'; +--- + + +
+

Vladimír Duša

+
+

+ Jsem [váš popis — např. vývojář, designér, pedagog...] se zájmem o [témata, kterým se věnujete]. + Na tomto webu sdílím své myšlenky, zkušenosti a postřehy z oblastí, které mě baví a zajímají. +

+

+ [Druhý odstavec — co vás formovalo, co děláte profesně nebo osobně, co vás žene dopředu.] +

+

+ [Třetí odstavec — jak vás mohou čtenáři kontaktovat, co zde najdou, nebo co je na tomto blogu jinak.] +

+
+
+
diff --git a/src/pages/cs/clanky/[slug].astro b/src/pages/cs/clanky/[slug].astro new file mode 100644 index 0000000..fa73181 --- /dev/null +++ b/src/pages/cs/clanky/[slug].astro @@ -0,0 +1,52 @@ +--- +import { getCollection } from 'astro:content'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { useTranslations } from '../../../i18n/ui'; + +const lang = 'cs'; +const t = useTranslations(lang); + +export async function getStaticPaths() { + const posts = await getCollection('blog', ({ data }) => !data.draft); + const csPosts = posts.filter(p => p.id.startsWith('cs/')); + return csPosts.map(post => { + const baseSlug = post.id.replace(/^cs\//, '').replace(/\.md$/, ''); + return { + params: { slug: baseSlug }, + props: { post }, + }; + }); +} + +const { post } = Astro.props; +const { slug } = Astro.params; +const { Content } = await post.render(); +--- + + +
+
+ {post.data.title} +
+

+ {post.data.date.toLocaleDateString('cs-CZ', { day: 'numeric', month: 'long', year: 'numeric' })} +

+

{post.data.title}

+

{post.data.description}

+
+ +
+ + {t('article.back')} + +
+
diff --git a/src/pages/cs/clanky/index.astro b/src/pages/cs/clanky/index.astro new file mode 100644 index 0000000..ce6f7e1 --- /dev/null +++ b/src/pages/cs/clanky/index.astro @@ -0,0 +1,101 @@ +--- +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import ArticleTile from '../../../components/ArticleTile.astro'; +import { getCollection } from 'astro:content'; +import { useTranslations } from '../../../i18n/ui'; + +const lang = 'cs'; +const t = useTranslations(lang); + +const posts = (await getCollection('blog', ({ data }) => !data.draft)) + .filter(p => p.id.startsWith('cs/')) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + +const allTags = [...new Set(posts.flatMap(p => p.data.tags))].sort(); +--- + + +
+ {allTags.length > 0 && ( +
+ + {allTags.map((tag) => ( + <> + · + + + ))} +
+ )} + +
+ {posts.map((post, i) => { + const baseSlug = post.id.replace(/^cs\//, '').replace(/\.md$/, ''); + return ( + + ); + })} +
+ {posts.length === 0 && ( +

{t('empty.articles')}

+ )} +
+
+ + + + diff --git a/src/pages/cs/hledat.astro b/src/pages/cs/hledat.astro new file mode 100644 index 0000000..d83f167 --- /dev/null +++ b/src/pages/cs/hledat.astro @@ -0,0 +1,139 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { useTranslations } from '../../i18n/ui'; + +const lang = 'cs'; +const t = useTranslations(lang); +--- + + +
+
+ +
+ + + +
    + + +
    +
    + + diff --git a/src/pages/cs/index.astro b/src/pages/cs/index.astro new file mode 100644 index 0000000..56a2d4e --- /dev/null +++ b/src/pages/cs/index.astro @@ -0,0 +1,101 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import ArticleTile from '../../components/ArticleTile.astro'; +import { getCollection } from 'astro:content'; +import { useTranslations } from '../../i18n/ui'; + +const lang = 'cs'; +const t = useTranslations(lang); + +const posts = (await getCollection('blog', ({ data }) => !data.draft)) + .filter(p => p.id.startsWith('cs/')) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + +const allTags = [...new Set(posts.flatMap(p => p.data.tags))].sort(); +--- + + + {posts.length === 0 ? ( +
    {t('empty.articles')}
    + ) : ( +
    + {allTags.length > 0 && ( +
    + + {allTags.map((tag) => ( + <> + · + + + ))} +
    + )} +
    + {posts.map((post, i) => { + const baseSlug = post.id.replace(/^cs\//, '').replace(/\.md$/, ''); + return ( + + ); + })} +
    +
    + )} +
    + + + + diff --git a/src/pages/cs/search.json.ts b/src/pages/cs/search.json.ts new file mode 100644 index 0000000..3304f6f --- /dev/null +++ b/src/pages/cs/search.json.ts @@ -0,0 +1,24 @@ +import { getCollection } from 'astro:content'; +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async () => { + const posts = (await getCollection('blog', ({ data }) => !data.draft)) + .filter(p => p.id.startsWith('cs/')) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + + const index = posts.map((post) => { + const slug = post.id.replace(/^cs\//, '').replace(/\.md$/, ''); + return { + slug, + title: post.data.title, + description: post.data.description, + image: post.data.image, + date: post.data.date.toISOString().split('T')[0], + body: post.body, + }; + }); + + return new Response(JSON.stringify(index), { + headers: { 'Content-Type': 'application/json' }, + }); +}; diff --git a/src/pages/en/articles/[slug].astro b/src/pages/en/articles/[slug].astro new file mode 100644 index 0000000..41cbe7c --- /dev/null +++ b/src/pages/en/articles/[slug].astro @@ -0,0 +1,63 @@ +--- +import { getCollection } from 'astro:content'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { useTranslations } from '../../../i18n/ui'; + +const lang = 'en'; +const t = useTranslations(lang); + +export async function getStaticPaths() { + const allPosts = await getCollection('blog', ({ data }) => !data.draft); + const csPosts = allPosts.filter(p => p.id.startsWith('cs/')); + const enPosts = allPosts.filter(p => p.id.startsWith('en/')); + + // Generate paths for ALL CS articles (EN articles without CS counterpart are also included) + return csPosts.map(csPost => { + const baseSlug = csPost.id.replace(/^cs\//, '').replace(/\.md$/, ''); + const enPost = enPosts.find(p => p.id.replace(/^en\//, '').replace(/\.md$/, '') === baseSlug); + return { + params: { slug: baseSlug }, + props: { post: enPost ?? csPost, hasTranslation: !!enPost }, + }; + }); +} + +const { post, hasTranslation } = Astro.props; +const { slug } = Astro.params; +const { Content } = await post.render(); + +const dateLocale = hasTranslation ? 'en-GB' : 'cs-CZ'; +--- + + +
    + {!hasTranslation && ( +
    + {t('article.no-translation')} +
    + )} +
    + {post.data.title} +
    +

    + {post.data.date.toLocaleDateString(dateLocale, { day: 'numeric', month: 'long', year: 'numeric' })} +

    +

    {post.data.title}

    +

    {post.data.description}

    +
    + +
    + + {t('article.back')} + +
    +
    diff --git a/src/pages/en/articles/index.astro b/src/pages/en/articles/index.astro new file mode 100644 index 0000000..47bd25a --- /dev/null +++ b/src/pages/en/articles/index.astro @@ -0,0 +1,111 @@ +--- +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import ArticleTile from '../../../components/ArticleTile.astro'; +import { getCollection } from 'astro:content'; +import { useTranslations } from '../../../i18n/ui'; + +const lang = 'en'; +const t = useTranslations(lang); + +const allPosts = await getCollection('blog', ({ data }) => !data.draft); +const csPosts = allPosts + .filter(p => p.id.startsWith('cs/')) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +const enPosts = allPosts.filter(p => p.id.startsWith('en/')); + +// For each CS post, find EN version if it exists +const articlesForDisplay = csPosts.map(csPost => { + const baseSlug = csPost.id.replace(/^cs\//, '').replace(/\.md$/, ''); + const enPost = enPosts.find(p => p.id.replace(/^en\//, '').replace(/\.md$/, '') === baseSlug); + return { + post: enPost ?? csPost, + slug: baseSlug, + hasTranslation: !!enPost, + }; +}); + +const allTags = [...new Set(articlesForDisplay.flatMap(a => a.post.data.tags))].sort(); +--- + + +
    + {allTags.length > 0 && ( +
    + + {allTags.map((tag) => ( + <> + · + + + ))} +
    + )} + +
    + {articlesForDisplay.map(({ post, slug, hasTranslation }, i) => ( + + ))} +
    + {articlesForDisplay.length === 0 && ( +

    {t('empty.articles')}

    + )} +
    +
    + + + + diff --git a/src/pages/en/bio.astro b/src/pages/en/bio.astro new file mode 100644 index 0000000..e43e1b2 --- /dev/null +++ b/src/pages/en/bio.astro @@ -0,0 +1,23 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; + +const lang = 'en'; +--- + + +
    +

    Vladimír Duša

    +
    +

    + I am [your description — e.g. developer, designer, educator...] with an interest in [topics you focus on]. + On this website I share my thoughts, experiences and observations from areas that interest and excite me. +

    +

    + [Second paragraph — what shaped you, what you do professionally or personally, what drives you forward.] +

    +

    + [Third paragraph — how readers can contact you, what they will find here, or what makes this blog different.] +

    +
    +
    +
    diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro new file mode 100644 index 0000000..f5ac0d3 --- /dev/null +++ b/src/pages/en/index.astro @@ -0,0 +1,111 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import ArticleTile from '../../components/ArticleTile.astro'; +import { getCollection } from 'astro:content'; +import { useTranslations } from '../../i18n/ui'; + +const lang = 'en'; +const t = useTranslations(lang); + +const allPosts = await getCollection('blog', ({ data }) => !data.draft); +const csPosts = allPosts + .filter(p => p.id.startsWith('cs/')) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +const enPosts = allPosts.filter(p => p.id.startsWith('en/')); + +// For each CS post, find EN version if it exists +const articlesForDisplay = csPosts.map(csPost => { + const baseSlug = csPost.id.replace(/^cs\//, '').replace(/\.md$/, ''); + const enPost = enPosts.find(p => p.id.replace(/^en\//, '').replace(/\.md$/, '') === baseSlug); + return { + post: enPost ?? csPost, + slug: baseSlug, + hasTranslation: !!enPost, + }; +}); + +const allTags = [...new Set(articlesForDisplay.flatMap(a => a.post.data.tags))].sort(); +--- + + + {articlesForDisplay.length === 0 ? ( +
    {t('empty.articles')}
    + ) : ( +
    + {allTags.length > 0 && ( +
    + + {allTags.map((tag) => ( + <> + · + + + ))} +
    + )} +
    + {articlesForDisplay.map(({ post, slug, hasTranslation }, i) => ( + + ))} +
    +
    + )} +
    + + + + diff --git a/src/pages/en/search.astro b/src/pages/en/search.astro new file mode 100644 index 0000000..0655012 --- /dev/null +++ b/src/pages/en/search.astro @@ -0,0 +1,143 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { useTranslations } from '../../i18n/ui'; + +const lang = 'en'; +const t = useTranslations(lang); +--- + + +
    +
    + +
    + + + +
      + + +
      +
      + + diff --git a/src/pages/en/search.json.ts b/src/pages/en/search.json.ts new file mode 100644 index 0000000..052c563 --- /dev/null +++ b/src/pages/en/search.json.ts @@ -0,0 +1,30 @@ +import { getCollection } from 'astro:content'; +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async () => { + const allPosts = await getCollection('blog', ({ data }) => !data.draft); + const csPosts = allPosts + .filter(p => p.id.startsWith('cs/')) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + const enPosts = allPosts.filter(p => p.id.startsWith('en/')); + + // For each CS article, use EN version if available, otherwise fall back to CS + const index = csPosts.map((csPost) => { + const baseSlug = csPost.id.replace(/^cs\//, '').replace(/\.md$/, ''); + const enPost = enPosts.find(p => p.id.replace(/^en\//, '').replace(/\.md$/, '') === baseSlug); + const post = enPost ?? csPost; + return { + slug: baseSlug, + title: post.data.title, + description: post.data.description, + image: post.data.image, + date: post.data.date.toISOString().split('T')[0], + body: post.body, + lang: enPost ? 'en' : 'cs', + }; + }); + + return new Response(JSON.stringify(index), { + headers: { 'Content-Type': 'application/json' }, + }); +}; diff --git a/src/pages/index.astro b/src/pages/index.astro index 561196b..0603b35 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,17 +1,3 @@ --- - +return Astro.redirect('/cs/'); --- - - - - - - - - - Astro - - -

      Astro

      - - diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..91eac08 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,138 @@ +@import "tailwindcss"; +@plugin "@tailwindcss/typography"; + +body { + background-color: #ffffff; + color: #111111; + -webkit-font-smoothing: antialiased; +} + +/* Letter-spaced name in header */ +.site-name { + letter-spacing: 0.18em; + font-weight: 400; + text-transform: uppercase; + font-size: 0.9rem; +} + +/* Article tile grid classes (must be global — applied to child component roots) */ +.tile-featured-left, +.tile-featured-right { + grid-column: span 2; +} + +@media (max-width: 480px) { + .tile-featured-left, + .tile-featured-right { + grid-column: span 1; + } +} + +/* Article tile grayscale → color on hover */ +.article-tile { + position: relative; + overflow: hidden; + display: block; + border-radius: 0.75rem; +} + +.article-tile img { + width: 100%; + height: 100%; + object-fit: cover; + filter: grayscale(100%); + transform: scale(1); + transition: filter 0.4s ease, transform 0.5s ease; +} + +.article-tile:hover img { + filter: grayscale(0%); + transform: scale(1.06); +} + +.article-tile .tile-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 1.25rem 1rem 1rem; + background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.5) 50%, transparent 100%); + color: white; +} + +.article-tile .tile-tags { + font-size: 0.6rem; + letter-spacing: 0.14em; + text-transform: uppercase; + opacity: 0.75; + margin-bottom: 0.3rem; + text-shadow: 0 1px 2px rgba(0,0,0,0.5); +} + +.article-tile .tile-title { + font-size: 0.9rem; + font-weight: 500; + line-height: 1.35; + text-shadow: 0 1px 3px rgba(0,0,0,0.5); +} + +.article-tile .tile-description { + font-size: 0.75rem; + opacity: 0.85; + margin-top: 0.2rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-shadow: 0 1px 2px rgba(0,0,0,0.5); +} + +/* Filtered-out tile */ +.article-tile.is-filtered { + display: none; +} + +/* Language switcher */ +.lang-switcher { display: flex; align-items: center; gap: 0; margin-left: 1.5rem; padding-left: 1.5rem; border-left: 1px solid #e5e5e5; } +.lang-switcher a { font-size: 0.65rem; letter-spacing: 0.15em; text-transform: uppercase; color: #aaa; text-decoration: none; transition: color 0.2s; } +.lang-switcher a:hover { color: #111; } +.lang-switcher a.active { color: #111; border-bottom: 1px solid #111; } +.lang-switcher-sep { color: #ddd; font-size: 0.75rem; margin: 0 0.4rem; } + +/* Tag filter bar */ +.tag-filter { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0; + padding: 0 0 1.5rem; +} + +.tag-filter-btn { + font-size: 0.7rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: #aaa; + background: none; + border: none; + border-bottom: 1px solid transparent; + cursor: pointer; + padding: 0.15rem 0; + transition: color 0.2s, border-color 0.2s; +} + +.tag-filter-btn:hover { + color: #111; +} + +.tag-filter-btn.active { + color: #111; + border-bottom-color: #111; +} + +.tag-filter-sep { + color: #ccc; + font-size: 0.75rem; + margin: 0 0.65rem; + user-select: none; +}