diff --git a/content/meta/index.mdoc b/content/meta/index.mdoc new file mode 100644 index 0000000..64aee48 --- /dev/null +++ b/content/meta/index.mdoc @@ -0,0 +1,71 @@ +--- +title: A shape of things to come +cover: + src: /images/covers/meta/index/cover/src.jpeg + alt: Pile of Skulls + caption: >- + To crush your players, see them driven before you, and to hear their + wailings of their characters + showInHeader: true +meta: + publicationDate: 2025-10-01T10:30:00.000Z + status: draft + isFeatured: true + tags: + - how-and-what + author: dave-damage +seo: + noIndex: false +--- +Vester cælestis pascit illa{% Sidenote #Test marker="⋄" content="In partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur" type="default" /%}. Nonne vos magis pluris estis illis? Quis autem vestrum cogitans potest adjicere ad staturam suam cubitum unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt non laborant, neque nent. Dico autem vobis, quoniam nec Salomon in omni gloria sua coopertus est sicut unum ex istis. Si autem fœnum agri, quod hodie est, et cras in clibanum mittitur, Deus sic vestit, quanto magis vos modicæ fidei? Nolite ergo solliciti esse, dicentes Quid manducabimus, aut quid bibemus, aut quo operiemur? hæc enim omnia gentes inquirunt. Scit enim Pater vester, quia his omnibus indigetis. Quærite. + +{% table %} +- Ubi +- Stell +--- +- 3986 +- 7176 +--- +- 7673 +- 3875 +{% /table %} + +## Videantur ab hominibus + +In partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes **multorum** Baptista prædicans in deserto Judææ, et dicens Pœnitentiam agite appropinquavit enim regnum cælorum. Hic est enim, qui dictus est per Isaiam prophetam dicentem Vox clamantis in deserto Parate viam Domini; rectas facite semitas ejus. Ipse autem Joannes habebat vestimentum **fiant qui** de pilis camelorum, et zonam pelliceam circa lumbos suos esca autem ejus erat locustæ, et mel silvestre. Tunc exibat ad eum Jerosolyma, et omnis Judæa. + +### Venti et mare obediunt + +- Discipulis ejus ait illi domine permitte. +- Inducas in tentationem sed libera nos a **pallium** malo. +- Recumbent cum abraham et isaac et +- Meum jesus autem ait illi sequere *omnis enim that small* me et. +- Sua nolite judicare ut not judicemini in +- Fidei tunc *altare et* **autem sermo vester** surgens imperavit. +- Es mitte te *infirmitatem* deorsum scriptum. +- Judorum vidimus enim stellam *dico vobis quia* ejus. + +#### Procedit de ore dei + +1. De vestimento **sua nolite judicare ut** quid solliciti **quoniam ipsi consolabuntur** estis considerate lilia. +1. Animam pueri qui consurgens accepit puerum et. +1. Dicens beati pauperes spiritu quoniam +1. Vobis quoniam nec salomon in. +1. Estis sal terr quod si *eleemosynam nesciat sinistra* sal evanuerit. +1. Jesus manum tetigit eum dicens volo [Ab eo](https://example.com/erant/fructum) *arcta is qu ducit* **in manu** mundare. +1. Dimisit eam febris et surrexit et ministrabat *dei* eis. +1. Tuus qui ==sic omnis== *per isaiam* videt in. + +##### Which dificavit domum suam + +Dico vobis quia omnis qui viderit mulierem ad concupiscendum eam, jam mœchatus est eam in corde suo. Quod si oculus tuus dexter scandalizat te, erue eum, et projice abs te expedit enim tibi ut pereat unum membrorum tuorum, quam totum corpus tuum mittatur in gehennam. Et si dextra manus tua scandalizat te, abscide eam, et projice abs te expedit enim tibi ut pereat unum membrorum tuorum, quam totum corpus tuum eat in gehennam. Dictum est autem Quicumque dimiserit uxorem suam, det ei libellum repudii. Ego autem dico vobis quia omnis. + +###### Malo est audistis + +Ipsi Deum videbunt. Beati pacifici quoniam filii Dei vocabuntur. Beati qui persecutionem patiuntur propter justitiam quoniam ipsorum est regnum cælorum. Beati estis cum maledixerint vobis, et persecuti vos fuerint, et dixerint omne malum adversum vos mentientes, propter me gaudete, et exsultate, quoniam merces vestra copiosa est in cælis. Sic enim persecuti sunt prophetas, qui fuerunt ante vos. Vos estis sal terræ. Quod si sal evanuerit, in quo salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. Non potest civitas abscondi. + +Prophetam dicentem dicens Surge, et accipe puerum, et *qui dmonia habebant et* matrem ejus, et vade in terram Israël defuncti sunt enim qui quærebant animam pueri. Qui consurgens, accepit puerum, et matrem **grotationes** ejus, et venit in terram Israël. Audiens autem quod Archelaus regnaret in Judæa **reddes autem domino juramenta** pro Herode patre suo, timuit illo ire et admonitus in somnis, secessit in partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est *in* per prophetas Quoniam Nazaræus vocabitur. In. + +Videns autem multos pharisæorum{% Sidenote #non-unique marker="*" content="In partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes multorum Baptista prædicans in deserto Judææ, et dicens Pœnitentiam agite appropinquavit enim regnum cælorum. Hic est enim, qui dictus est per Isaiam prophetam dicentem Vox clamantis in deserto Parate viam Domini; rectas facite semitas ejus. Ipse autem Joannes habebat vestimentum fiant qui de pilis camelorum, et zonam pelliceam circa lumbos suos esca autem ejus erat locustæ, et mel silvestre. Tunc exibat ad eum Jerosolyma, et omnis Judæa." type="default" /%}, et sadducæorum, venientes ad baptismum suum, dixit eis Progenies viperarum, quis demonstravit vobis fugere a ventura ira? Facite ergo fructum dignum pœnitentiæ. Et ne velitis dicere intra vos Patrem habemus Abraham. Dico enim vobis quoniam potens est Deus de lapidibus istis suscitare filios Abrahæ. Jam enim securis ad radicem arborum posita est. Omnis ergo arbor, quæ non facit fructum bonum, excidetur, et in ignem mittetur. Ego **flumina** quidem baptizo vos **mentientes propter** in aqua in. + +Sal terræ. Quod si sal evanuerit, in quo salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. Non potest civitas abscondi supra montem posita, neque accendunt lucernam, et ponunt eam sub modio, sed super candelabrum, ut luceat omnibus qui in domo sunt. Sic luceat lux vestra coram hominibus ut videant opera vestra bona, **admirabantur turb super doctrina** et glorificent Patrem vestrum, qui in cælis est. Nolite putare quoniam veni solvere legem aut prophetas non veni solvere, sed adimplere. Amen quippe dico vobis, donec transeat cælum et terra, jota unum aut unus apex non præteribit **jussit** a. diff --git a/content/system/navigation.json b/content/system/navigation.json index 1d4bb5e..0a9e67f 100644 --- a/content/system/navigation.json +++ b/content/system/navigation.json @@ -14,7 +14,7 @@ }, { "name": "Kitchensink", - "path": "/", + "path": "/kitchensink", "gridPosition": "area_2", "variant": "kitchensink", "sublinks": { @@ -123,17 +123,17 @@ "path": "/chainbreaker", "gridPosition": "area_5", "variant": "chainbreaker", - "background": "/images/categories/items/4/background.png", + "background": "/images/categories/items/4/background.jpg", "sublinks": { "discriminant": false }, "subtitle": { "discriminant": true, "value": { - "content": "Spear & Animism", + "content": "A history of Violence", "divider": { "discriminant": true, - "value": "⎊" + "value": "⨳" } } } diff --git a/content/taxonomy/authors/dave-damage.json b/content/taxonomy/authors/dave-damage.json new file mode 100644 index 0000000..8475e80 --- /dev/null +++ b/content/taxonomy/authors/dave-damage.json @@ -0,0 +1,15 @@ +{ + "name": "Dave Damage", + "avatar": "/images/authors/dave-damage/avatar.png", + "description": "Nothing nice 2 say", + "contacts": [ + { + "type": "email", + "url": "ottonom@gmail.com" + }, + { + "type": "discord", + "url": "381079209197699083" + } + ] +} diff --git a/content/taxonomy/tags/how-and-what.json b/content/taxonomy/tags/how-and-what.json new file mode 100644 index 0000000..9125c77 --- /dev/null +++ b/content/taxonomy/tags/how-and-what.json @@ -0,0 +1,8 @@ +{ + "name": "Was ist Was?", + "icon": { + "discriminant": "glyph", + "value": "‽" + }, + "description": "General information and introductions for stuff" +} diff --git a/keystatic.config.ts b/keystatic.config.ts index 3243832..efc569c 100644 --- a/keystatic.config.ts +++ b/keystatic.config.ts @@ -1,11 +1,18 @@ import { config } from '@keystatic/core'; -import navigation from '@/keystatic/singletons/navigation'; +import NavSingleton from '@/keystatic/singletons/navigation'; +import AuthorsCollection from '@/keystatic/collections/taxonomy/authors'; +import TagsCollection from '@/keystatic/collections/taxonomy/tags'; +import MetaPostsCollection from '@/keystatic/collections/meta/article'; export default config({ storage: { kind: 'local', }, - collections: {}, - singletons: { navigation }, + singletons: { navigation: NavSingleton }, + collections: { + authors: AuthorsCollection, + tags: TagsCollection, + meta_posts: MetaPostsCollection, + }, }); diff --git a/package.json b/package.json index 56daf32..e87df04 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "format": "prettier --write ." }, "dependencies": { + "@keystar/ui": "^0.7.19", "@keystatic/core": "^0.5.48", "@keystatic/next": "^5.0.4", "@markdoc/markdoc": "^0.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2693a7d..c55632b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@keystar/ui': + specifier: ^0.7.19 + version: 0.7.19(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@keystatic/core': specifier: ^0.5.48 version: 0.5.48(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) diff --git a/public/images/authors/dave-damage/avatar.png b/public/images/authors/dave-damage/avatar.png new file mode 100644 index 0000000..13f1186 Binary files /dev/null and b/public/images/authors/dave-damage/avatar.png differ diff --git a/public/images/categories/items/4/background.jpg b/public/images/categories/items/4/background.jpg new file mode 100644 index 0000000..8945997 Binary files /dev/null and b/public/images/categories/items/4/background.jpg differ diff --git a/public/images/categories/items/4/background.png b/public/images/categories/items/4/background.png deleted file mode 100644 index feb1e55..0000000 Binary files a/public/images/categories/items/4/background.png and /dev/null differ diff --git a/public/images/covers/meta/index/cover/src.jpeg b/public/images/covers/meta/index/cover/src.jpeg new file mode 100644 index 0000000..6f5d466 Binary files /dev/null and b/public/images/covers/meta/index/cover/src.jpeg differ diff --git a/public/images/jon-butterworth-rXjSnBx8Kuc-unsplash.jpg b/public/images/jon-butterworth-rXjSnBx8Kuc-unsplash.jpg new file mode 100644 index 0000000..8945997 Binary files /dev/null and b/public/images/jon-butterworth-rXjSnBx8Kuc-unsplash.jpg differ diff --git a/public/images/pexels-chalta-phirta-307182428-32933283.jpg b/public/images/pexels-chalta-phirta-307182428-32933283.jpg new file mode 100644 index 0000000..8eb1119 Binary files /dev/null and b/public/images/pexels-chalta-phirta-307182428-32933283.jpg differ diff --git a/public/images/pexels-clickerhappy-534590.jpg b/public/images/pexels-clickerhappy-534590.jpg new file mode 100644 index 0000000..63060ce Binary files /dev/null and b/public/images/pexels-clickerhappy-534590.jpg differ diff --git a/public/images/pexels-david-bartus-43782-450100.jpg b/public/images/pexels-david-bartus-43782-450100.jpg new file mode 100644 index 0000000..ccac538 Binary files /dev/null and b/public/images/pexels-david-bartus-43782-450100.jpg differ diff --git a/public/images/pexels-deann-dasilva-1997359-6440968.jpg b/public/images/pexels-deann-dasilva-1997359-6440968.jpg new file mode 100644 index 0000000..b45aabc Binary files /dev/null and b/public/images/pexels-deann-dasilva-1997359-6440968.jpg differ diff --git a/public/images/pexels-igor-haritanovich-814387-1695050.jpg b/public/images/pexels-igor-haritanovich-814387-1695050.jpg new file mode 100644 index 0000000..d99279f Binary files /dev/null and b/public/images/pexels-igor-haritanovich-814387-1695050.jpg differ diff --git a/public/images/pile-of-skulls.jpeg b/public/images/pile-of-skulls.jpeg new file mode 100644 index 0000000..6f5d466 Binary files /dev/null and b/public/images/pile-of-skulls.jpeg differ diff --git a/public/images/swamp.jpeg b/public/images/swamp.jpeg new file mode 100644 index 0000000..16e5e9e Binary files /dev/null and b/public/images/swamp.jpeg differ diff --git a/src/app/(site)/layout.tsx b/src/app/(site)/layout.tsx index 6864755..0792372 100644 --- a/src/app/(site)/layout.tsx +++ b/src/app/(site)/layout.tsx @@ -9,7 +9,7 @@ export default function SiteLayout({ -
{children}
+ {children}
); } diff --git a/src/app/(site)/meta/page.tsx b/src/app/(site)/meta/page.tsx new file mode 100644 index 0000000..cfb58a6 --- /dev/null +++ b/src/app/(site)/meta/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; + +import { Author, Tag } from '@/lib/types/taxonomy'; +import Article from '@/components/Article'; +import { getMetaHome } from '@/lib/readers/meta/posts'; +import generatePageMetaData from '@/lib/next/generatePageMetaData'; +import { getAuthorBySlug } from '@/lib/readers/taxonomy/authors'; +import { getTagBySlug } from '@/lib/readers/taxonomy/tags'; + +export async function generateMetadata(): Promise { + const article = await getMetaHome(); + + if (!article) return { title: 'Not Found' }; + + return generatePageMetaData(article); +} + +export default async function MetaHome() { + const article = await getMetaHome(); + if (!article) notFound(); + + const author = await getAuthorBySlug(article.meta.author); + const tags = await Promise.all( + article.meta.tags.map((slug: string) => getTagBySlug(slug)) + ); + + return
; +} diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx index 208a92a..77fcea5 100644 --- a/src/app/(site)/page.tsx +++ b/src/app/(site)/page.tsx @@ -6,488 +6,6 @@ export default function Home() {

DAVE! DAVE! Do Not Let Us Die In The Dark Night Of This Cold Winter!

-

Background / Image effects

-
-
-

Overexposure Blast

-
- Demo -
-
- -
-

Contrast Slam

-
- Demo -
-
- -
-

Colorbleed

-
- Demo -
-
- -
-

Film Burn

-
- Demo -
-
- -
-

Photocopier Malfunction

-
- Demo -
-
- -
-

Toner Starvation

-
- Demo -
-
- -
-

Digital Corruption

-
- Demo -
-
- -
-

Stark Flash

-
- Demo -
-
-
- -
-

Instructions

-

- Hover over each image to see the effect. These are designed to work - with your base filter: -

- filter: grayscale(1) contrast(150%) brightness(120%) -

- You can replace the demo images with your own by changing the src - attributes. All effects use stepped animations or sharp transitions to - maintain that industrial, non-digital feel. -

-

- The effects range from subtle (Contrast Slam) to more dramatic - (Digital Corruption). Choose based on how aggressive you want the - interaction to feel. -

-
-

Link Effects

-
- {/* Your Ideas */} -
-

Strikethrough Mark (Marker)

-

- This is some text with a{' '} - - strikethrough link - {' '} - in the middle of it. -

-
-
-

Strikethrough Mark (Industrial)

-

- This is some text with a{' '} - - strikethrough link - {' '} - in the middle of it. -

-
-
-

Marker Highlight [Industrial]

-

- This is some text with a{' '} - - marker highlight link - {' '} - in the middle of it. -

-
- - {/* Stamping/Punching Effects */} -
-

Label Maker

-

- This is some text with a{' '} - - label maker link - {' '} - in the middle of it. -

-
- -
-

Rubber Stamp

-

- This is some text with a{' '} - - rubber stamp link - {' '} - in the middle of it. -

-
- - {/* Industrial/Mechanical */} -
-

Press/Stamp

-

- This is some text with a{' '} - - pressed link - {' '} - in the middle of it. -

-
- -
-

Typewriter Underline

-

- This is some text with a{' '} - - typewriter link - {' '} - in the middle of it. -

-
- -
-

Hard Invert

-

- This is some text with a{' '} - - hard invert link - {' '} - in the middle of it. -

-
- - {/* Marking/Annotation */} -
-

Bracket Annotation

-

- This is some text with a{' '} - - bracket link - {' '} - in the middle of it. -

-
- -
-

Corner Box

-

- This is some text with a{' '} - - corner box link - {' '} - in the middle of it. -

-
- - {/* Glitch/Digital */} -
-

Character Glitch

-

- This is some text with a{' '} - - glitch link - {' '} - in the middle of it. -

-
- -
-

Pixel Shift

-

- This is some text with a{' '} - - shifting link - {' '} - in the middle of it. -

-
- - {/* Extra Ideas */} -
-

Redacted/Censored

-

- This is some text with a{' '} - - redacted link - {' '} - in the middle of it. -

-
- -
-

X-Ray/Negative

-

- This is some text with an{' '} - - x-ray link - {' '} - in the middle of it. -

-
-
-
-

- Successful Isildur's brink again throttle flank tightening splash. -

-

- Help illusion embrace liquor tightening intelligence Maggot's whips - bit forests. 17 sing impassable helps Southrons beheading. What's the - Elvish word for 'friend'? -

-

Give Hobbitses lend yours lads picking uniting sometime.

-

- Inferno shaken skin undo wars close circles verse suck Dwarves. I gave - you the chance of aiding me willingly, but you have elected the way of - pain! Precautions tower tied Rivendell everyone agents wouldn't? -

-

Doorway Mithrandir clearing wielder strengths floor?

-

- Pillaged pointy-eared mix charm Grond confounded able-bodied tact - glimpse instruction open dear. Suffering powerful capable gulls - famousest stroke breathes Bilbo squeaked pace chances. Let the - Ring-bearer decide. -

-
    -
  • Mirkwood.
  • -
  • Tom.
  • -
  • Bilbo's.
  • -
  • Gandalf's.
  • -
  • Dwarvish.
  • -
-
    -
  • Queen wants Oin loose heads decay piety!
  • -
  • Large happening arrived owes legends wit war bled Durin's.
  • -
  • - Pursuit exactly during relief mission meats cause Noldorin ablaze - tracked. -
  • -
  • Darken knife midday meat Goblinses.
  • -
  • - Your Rabble-rousers greatest could beast thirty-four t wizards - slumbers reforge. -
  • -
-

Late vagabond knowing Ent legends there flattened cultured?

-

- Swords are no more use here. Deny Chubbs restored. Scare rebuild - Argonath tracked day's large. -

-
    -
  1. Easterlings!
  2. -
  3. Bill.
  4. -
  5. Dúnedain!
  6. -
  7. Gandalf.
  8. -
  9. Orcrist.
  10. -
-
    -
  1. Answers feelings Elrond conjurer runs.
  2. -
  3. Sul nudge powerless jelly dumping hair grows log forgave?
  4. -
  5. - Retaken succumbed funeral courtyard Glóin incident mere somewhere - commander assistance? -
  6. -
  7. Bore outrun stead fight Athelas guardroom willing contains.
  8. -
  9. Hunted Angmar wager noose arguing?
  10. -
-

Elros next own wisp whence cakehole right!

-

- Thank hid its lessened lined tells prefers were Stone-Giants thousand - troubles. Earendil staff pines bog finest mushroom consumption. - Mistaken streaming fates paths arts puppet Barad-dûr uniting? You - shall not pass! -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AlesDeeplyMortalityOpenSingle-handedly
Bandappearedwaitedwhoseplatemarshaling
Meatnestthatchedrallyingclaimedhungers
Mousealsobirdsesmoonsstrainbrightest
Hatedifferentarrangementschiefestthinktry
Something'staskhere'sdecentsomeoneuses
Shirttonightbaybeautifullytadcloaks
revengeteachingmischiefshoresdreamstested
-

Stage dishcloth 20 horses Tooks souls crawl crime.

-

- Approve Sul wilderness grave embellishment greatly over these rack - struggle. End ringing bell Anor halls pairs thirst fortress curtain - cleared? Dwelt language 1296 Underhill nasty. I gave you the chance of - aiding me willingly, but you have elected the way of pain! -

-
-

You are full of surprises, Master Baggins.

-
- —Girion, warmongering appears -
-
-
- Sung outscoring fingers Fundin reaction inquiries buggers deadliest. -
-

- Mustn't powerless pierces Muil sorry crossing diamond brandy. Rip - rockets hinder Braga go had web ought sakes hail. A wizard is never - late, Frodo Baggins. Nor is he early. He arrives precisely when he - means to. Ashes tie Gamgee dicky! -

-
-          Rhudaur fancy tilled heart beggars. Dwarf nothing talked foot club.
-          Slaughtered flatten Hobbit journey's four-day?
-        
-
Pearl tact tomb bits Arwen Evenstar worry?
-

- Times unspoiled Silvan.{' '} - Sigrid Pippin Gandalf thin stubbornness noises easily - spread. Eldar warriors won answered filth yourself - pocket. Showing store consistency M crevice. Decision - feverfew giving Misty Mountain lord supplant. Gorgoroth{' '} - load born fulfilled plenty fates serpent. Doorstep Pippin's{' '} - pity bridge long weak weep? Brightest Chubbs{' '} - jewels understand. Somewhat Erebor noise squealing moved? - Pippin's feels overrun hours brown burns. Anor{' '} - turning pick prophecy. Surrounded entered needlessly weary vile - hmm Bagshot Row. Consent outwitted dotage slug Homely hear. - Parapet protected favored defied roam quiet Dori sick - bent. Homage store hurricane prove ferret Helm's Deep - lately? Excellent regret fun often returned Wood-elves - apocalypse. Théodred's rights rat drawing{' '} - examine dared bygone residence deeply.{' '} - Greenway Girion Rohirrim trammel waiting edge. -

-
-
-
Turn
-
- Bare protuberance arrived forging funny salvage Cair except first - banners. -
-
Foes
-
Bore river large house shadows it's Tuckborough warn.
-
- Stirring Greenwood nest sapphire grant gob flagon famous mean! -
-
Unprepared
-
- Single-handed wriggling creatures lock canopy anytime horses defense - Hobbit's? -
-
- Nûmenor dungeons achieving encourage fretting dines believes - understand. -
-
Ease love shine legs wee harbor Udùn adventure tumble stays.
-
-
); } diff --git a/src/components/Article/Article.module.css b/src/components/Article/Article.module.css new file mode 100644 index 0000000..b253f8f --- /dev/null +++ b/src/components/Article/Article.module.css @@ -0,0 +1,3 @@ +.article { + @mixin layout-wrapper; +} \ No newline at end of file diff --git a/src/components/Article/Content/MarkdocRenderer/MarkdocRenderer.module.css b/src/components/Article/Content/MarkdocRenderer/MarkdocRenderer.module.css new file mode 100644 index 0000000..ee76c5c --- /dev/null +++ b/src/components/Article/Content/MarkdocRenderer/MarkdocRenderer.module.css @@ -0,0 +1,23 @@ +.wrapper { + @mixin mx auto; + @mixin px var(--spacing-comfortable); + + max-width: 90ch; + font-size: clamp(1rem, 2.5vw, 1.5rem); +} + +.hasMargin { + @media screen and (--bp-margin) { + @mixin px 0; + + display: flex; + gap: var(--spacing-generous); + max-width: none; + + & .content { + flex: 1 1 auto; + min-width: 50ch; + max-width: 75ch; + } + } +} \ No newline at end of file diff --git a/src/components/Article/Content/MarkdocRenderer/index.tsx b/src/components/Article/Content/MarkdocRenderer/index.tsx new file mode 100644 index 0000000..2cc2047 --- /dev/null +++ b/src/components/Article/Content/MarkdocRenderer/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import Markdoc from '@markdoc/markdoc'; +import type { Node } from '@markdoc/markdoc'; + +import styles from './MarkdocRenderer.module.css'; + +import { nodes } from '@/lib/markdoc/nodes'; +import { tags } from '@/lib/markdoc/tags'; + +import { collectSideNotes, hasComponents } from '@/lib/markdoc/utils'; + +import { Column } from '@/components/Content/Grid/Column'; +import { Row } from '@/components/Content/Grid/Row'; +import Sidenote from '@/components/Content/Sidenote/Item'; +import SidenoteContainer from '@/components/Content/Sidenote/Container'; + +interface MarkdocRendererProps { + content: () => Promise<{ node: Node }>; + className?: string; +} + +const MARGIN_COMPONENTS = ['Sidenote']; + +export default async function MarkdocRenderer({ + content, + className, +}: MarkdocRendererProps) { + const components = { + Column, + Row, + Sidenote, + }; + + const { node } = await content(); + const errors = Markdoc.validate(node, { tags, nodes }); + + if (errors.length) { + console.error('Markdoc validation errors:', errors); + throw new Error('Invalid Content'); + } + + const renderable = Markdoc.transform(node, { tags, nodes }); + const sidenotes = collectSideNotes(renderable); + const showMargin = hasComponents(renderable, MARGIN_COMPONENTS); + + return ( +
+
+ {Markdoc.renderers.react(renderable, React, { components })} +
+ +
+ ); +} diff --git a/src/components/Article/Content/index.tsx b/src/components/Article/Content/index.tsx new file mode 100644 index 0000000..8338705 --- /dev/null +++ b/src/components/Article/Content/index.tsx @@ -0,0 +1,10 @@ +import MarkdocRenderer from '@/components/Article/Content/MarkdocRenderer'; +import { ArticleContent } from '@/lib/types/content'; + +interface ContentProps { + article: ArticleContent; +} + +export default function Content({ article }: ContentProps) { + return ; +} diff --git a/src/components/Article/Footer/Footer.module.tsx b/src/components/Article/Footer/Footer.module.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Footer/index.tsx b/src/components/Article/Footer/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Header/Cover/Cover.module.css b/src/components/Article/Header/Cover/Cover.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Header/Cover/index.tsx b/src/components/Article/Header/Cover/index.tsx new file mode 100644 index 0000000..7a61688 --- /dev/null +++ b/src/components/Article/Header/Cover/index.tsx @@ -0,0 +1,36 @@ +import Image from 'next/image'; + +import styles from './Cover.module.css'; + +interface CoverProps { + cover: { + readonly src: string; + readonly alt: string; + readonly caption: string; + readonly showInHeader: boolean; + }; +} + +export default function Cover({ cover }: CoverProps) { + return ( +
+ {cover.alt +
+
+ {cover.caption ? ( + {cover.caption} + ) : ( + {cover.src} + )} +
+
+
+ ); +} diff --git a/src/components/Article/Header/Header.module.css b/src/components/Article/Header/Header.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Header/Meta/Meta.module.css b/src/components/Article/Header/Meta/Meta.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Header/Meta/index.tsx b/src/components/Article/Header/Meta/index.tsx new file mode 100644 index 0000000..9d723b6 --- /dev/null +++ b/src/components/Article/Header/Meta/index.tsx @@ -0,0 +1,60 @@ +import Link from 'next/link'; + +import { Author, Tag } from '@/lib/types/taxonomy'; +import { toMilitaryDTG } from '@/lib/utils/date'; + +import styles from './Meta.module.css'; + +interface MetaProps { + author: Author | null; + tags: Tag[]; + updateDate?: string; + publicationDate: string; +} + +export default function Meta({ + author, + tags, + updateDate, + publicationDate, +}: MetaProps) { + return ( +
+ {author && ( +
+ Author + + {author.name} + +
+ )} + {tags && tags.length > 0 && ( +
+ Tags + +
    + {tags.map((tag) => ( +
  • + + {tag.name} + +
  • + ))} +
+
+
+ )} + {publicationDate && ( +
+ Last Update + +
+ )} +
+ ); +} diff --git a/src/components/Article/Header/Overline/Overline.module.css b/src/components/Article/Header/Overline/Overline.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Header/Overline/index.tsx b/src/components/Article/Header/Overline/index.tsx new file mode 100644 index 0000000..b5a085f --- /dev/null +++ b/src/components/Article/Header/Overline/index.tsx @@ -0,0 +1,46 @@ +import Link from 'next/link'; + +import styles from './Overline.module.css'; +import { toMilitaryDTG } from '@/lib/utils/date'; + +interface OverlineProps { + breadcrumbs?: string[]; + publicationDate: string; +} + +export default function Overline({ + breadcrumbs = [], + publicationDate, +}: OverlineProps) { + const breadcrumber = breadcrumbs.map((crumb, index) => { + const isLast = index === breadcrumbs.length - 1; + const href = `/${breadcrumbs.slice(0, index + 1).join('/')}`; + return ( +
  • + / + {isLast ? ( + {crumb} + ) : ( + + {crumb} + + )} +
  • + ); + }); + return ( +
    +
      +
    • + + dave-dmg.de + +
    • + {breadcrumber} +
    + +
    + ); +} diff --git a/src/components/Article/Header/Title/Title.module.css b/src/components/Article/Header/Title/Title.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Article/Header/Title/index.tsx b/src/components/Article/Header/Title/index.tsx new file mode 100644 index 0000000..a856bd3 --- /dev/null +++ b/src/components/Article/Header/Title/index.tsx @@ -0,0 +1,14 @@ +import styles from './Title.module.css'; +import { JSX } from 'react'; + +interface TitleProps { + title: string; +} + +export default function Title({ title }: TitleProps) { + return ( +
    +

    {title}

    +
    + ); +} diff --git a/src/components/Article/Header/index.tsx b/src/components/Article/Header/index.tsx new file mode 100644 index 0000000..bda65cb --- /dev/null +++ b/src/components/Article/Header/index.tsx @@ -0,0 +1,41 @@ +import { ArticleContent } from '@/lib/types/content'; +import { Author, Tag } from '@/lib/types/taxonomy'; +import { isValidCover } from '@/lib/utils/guards'; + +import styles from './Header.module.css'; +import Overline from '@/components/Article/Header/Overline'; +import Title from '@/components/Article/Header/Title'; +import Meta from '@/components/Article/Header/Meta'; +import Cover from '@/components/Article/Header/Cover'; + +interface HeaderProps { + article: ArticleContent; + breadcrumbs?: string[]; + author: Author | null; + tags: Tag[]; +} + +export default function Header({ + article, + breadcrumbs, + author, + tags, +}: HeaderProps) { + const { title, meta, cover } = article; + return ( +
    + + + {cover && isValidCover(cover) && <Cover cover={cover} />} + <Meta + tags={tags} + author={author} + updateDate={meta.updateDate} + publicationDate={meta.publicationDate} + /> + </header> + ); +} diff --git a/src/components/Article/index.tsx b/src/components/Article/index.tsx new file mode 100644 index 0000000..93ee838 --- /dev/null +++ b/src/components/Article/index.tsx @@ -0,0 +1,32 @@ +import type { ArticleContent } from '@/lib/types/content'; +import { Author, Tag } from '@/lib/types/taxonomy'; +import Header from './Header'; +import Content from '@/components/Article/Content'; + +import styles from './Article.module.css'; + +interface ArticleProps { + article: ArticleContent; + author: Author | null; + tags: Tag[]; + breadcrumbs?: string[]; +} + +export default function Article({ + article, + author, + tags, + breadcrumbs, +}: ArticleProps) { + return ( + <article className={styles.article}> + <Header + article={article} + author={author} + tags={tags} + breadcrumbs={breadcrumbs} + /> + <Content article={article} /> + </article> + ); +} diff --git a/src/components/Content/Grid/Column/Column.module.css b/src/components/Content/Grid/Column/Column.module.css new file mode 100644 index 0000000..df304ad --- /dev/null +++ b/src/components/Content/Grid/Column/Column.module.css @@ -0,0 +1,3 @@ +.column { + position: relative; +} \ No newline at end of file diff --git a/src/components/Content/Grid/Column/index.tsx b/src/components/Content/Grid/Column/index.tsx new file mode 100644 index 0000000..b0b6186 --- /dev/null +++ b/src/components/Content/Grid/Column/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import styles from './Column.module.css'; + +interface ColumnProps { + style: { + colspan: number; + }; + children?: React.ReactNode; +} + +export function Column({ style, children }: ColumnProps) { + return ( + <div + className={styles.column} + style={{ + gridColumn: `span ${style.colspan}`, + }} + > + {children} + </div> + ); +} diff --git a/src/components/Content/Grid/Row/Row.module.css b/src/components/Content/Grid/Row/Row.module.css new file mode 100644 index 0000000..48a2289 --- /dev/null +++ b/src/components/Content/Grid/Row/Row.module.css @@ -0,0 +1,3 @@ +.row { + display: grid; +} \ No newline at end of file diff --git a/src/components/Content/Grid/Row/index.tsx b/src/components/Content/Grid/Row/index.tsx new file mode 100644 index 0000000..c02c251 --- /dev/null +++ b/src/components/Content/Grid/Row/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import styles from './Row.module.css'; + +interface RowProps { + style: { + cols: number; + gap: string; + align: string; + justify: string; + }; + children: React.ReactNode; +} + +export function Row({ style, children }: RowProps) { + return ( + <div + className={`${styles.row}`} + style={{ + gridTemplateColumns: `repeat(${style.cols}, 1 fr)`, + gap: `${style.gap}`, + alignItems: `${style.align}`, + justifyContent: `${style.justify}`, + }} + > + {children} + </div> + ); +} diff --git a/src/components/Content/Sidenote/Container/Container.module.css b/src/components/Content/Sidenote/Container/Container.module.css new file mode 100644 index 0000000..8067b2b --- /dev/null +++ b/src/components/Content/Sidenote/Container/Container.module.css @@ -0,0 +1,63 @@ +.container { + flex: 1 0 auto; + min-width: 16rem; +} + +.wrapper { + @media screen and (--bp-margin) { + position: absolute; + top: anchor(top); + } +} + +.note { + position: relative; + min-width: 16rem; + font-family: var(--font-mono); + + @media screen and (--bp-margin) { + @mixin text-xs; + + margin-right: var(--spacing-generous); + padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ; + border: var(--size-1) solid var(--color-surface-inverse); + + background: var(--color-surface-elevated-1); + box-shadow: 2px 2px 0 var(--color-palette-woodsmoke), 4px 4px 0 var(--color-palette-charcoal-gray); + } +} + +.marker { + position: absolute; + top: 0; + bottom: 0; + left: 0; + + display: flex; + align-items: center; + justify-content: center; + + width: var(--size-8); + + font-size: var(--typo-size-3xl); + font-weight: var(--typo-weight-black); + color: var(--color-text-inverse); + + background-color: var(--color-surface-inverse); +} + +.content { + text-align: left; +} + +.ref { + @mixin ml var(--spacing-snug); + @mixin anim-txt-characterglitch; + + font-weight: var(--typo-weight-black); + color: var(--color-text-quarternary); + + &:hover { + color: var(--color-primary); + } +} \ No newline at end of file diff --git a/src/components/Content/Sidenote/Container/index.tsx b/src/components/Content/Sidenote/Container/index.tsx new file mode 100644 index 0000000..c4c38ea --- /dev/null +++ b/src/components/Content/Sidenote/Container/index.tsx @@ -0,0 +1,33 @@ +import type { Sidenote } from '@/lib/types/components'; +import styles from './Container.module.css'; + +interface ContainerProps { + items: Sidenote[]; +} + +export default function Container({ items }: ContainerProps) { + return ( + <aside className={styles.container}> + {items.map((sidenote: Sidenote) => ( + <div + key={sidenote.id} + id={sidenote.id} + className={styles.wrapper} + style={{ positionAnchor: `--note-${sidenote.id}` }} + > + <div className={`${styles.note} ${styles[sidenote.type]}`}> + <span className={styles.marker}> + <span className={styles.symbol}>{sidenote.marker}</span> + </span> + <div className={styles.content}> + {sidenote.content} + <a href={`#ref-${sidenote.id}`} className={styles.ref}> + ↩ + </a> + </div> + </div> + </div> + ))} + </aside> + ); +} diff --git a/src/components/Content/Sidenote/Item/Item.module.css b/src/components/Content/Sidenote/Item/Item.module.css new file mode 100644 index 0000000..adbb11d --- /dev/null +++ b/src/components/Content/Sidenote/Item/Item.module.css @@ -0,0 +1,11 @@ +.ref { + @mixin px var(--spacing-tight); + + display: inline-block; + font-weight: var(--typo-weight-black); + transition: color 0.5s ease-in-out; + + &:hover { + color: var(--color-primary); + } +} \ No newline at end of file diff --git a/src/components/Content/Sidenote/Item/index.tsx b/src/components/Content/Sidenote/Item/index.tsx new file mode 100644 index 0000000..e6bb94b --- /dev/null +++ b/src/components/Content/Sidenote/Item/index.tsx @@ -0,0 +1,17 @@ +import type { Sidenote } from '@/lib/types/components'; +import styles from './Item.module.css'; + +export default function SidenoteItem({ id, marker, content, type }: Sidenote) { + return ( + <sup> + <a + href={`#${id}`} + className={styles.ref} + id={`#ref-${id}`} + style={{ anchorName: `--note-${id}` }} + > + [{marker}] + </a> + </sup> + ); +} diff --git a/src/components/Page/Header/Header.module.css b/src/components/Page/Header/Header.module.css index b5f7ecd..6528a10 100644 --- a/src/components/Page/Header/Header.module.css +++ b/src/components/Page/Header/Header.module.css @@ -14,7 +14,7 @@ } .inner { - @mixin responsive-wrapper; + @mixin layout-wrapper; display: flex; flex-direction: row; diff --git a/src/components/Page/Menu/MenuArea/MenuArea.module.css b/src/components/Page/Menu/MenuArea/MenuArea.module.css index d43f901..3b02e29 100644 --- a/src/components/Page/Menu/MenuArea/MenuArea.module.css +++ b/src/components/Page/Menu/MenuArea/MenuArea.module.css @@ -60,7 +60,8 @@ --area-bg-animation-keyframe: none; --area-bg--animation-duration: 0s; --area-bg--animation-timing: linear; - --area-bg-filter: grayscale(100%) contrast(150) brightness(150); + --area-bg-filter: grayscale(100%) contrast(10) brightness(250); + --area-bg-blendmode: color-dodge; position: absolute; z-index: -1; @@ -70,6 +71,7 @@ object-fit: cover; filter: var(--area-bg-filter); + mix-blend-mode: var(--area-bg-blendmode); @media screen and (--bp-tablet-down) { display: none; @@ -93,27 +95,10 @@ --subtitle-transition: all 0.3s ease-in-out; --title-transition: color 0.3s ease-in-out; - &::before { - content: ""; - - position: absolute; - z-index: -1; - - width: 100%; - height: 100%; - - opacity: 0; - background: alpharize(var(--grid-bg), 0.66); - } - &:hover { --divider-color: var(--color-tertiary); --title-color: var(--color-tertiary); --subtitle-color: var(--color-tertiary); - - &::before { - opacity: 1; - } } } } diff --git a/src/components/Page/Menu/MenuGrid/MenuGrid.module.css b/src/components/Page/Menu/MenuGrid/MenuGrid.module.css index 6e22f62..a8eec5d 100644 --- a/src/components/Page/Menu/MenuGrid/MenuGrid.module.css +++ b/src/components/Page/Menu/MenuGrid/MenuGrid.module.css @@ -2,6 +2,7 @@ .menu { --grid-bg: var(--color-palette-charcoal-gray); --grid-fg: var(--color-palette-light-silver); + --area-blendmode: normal; /* === MenuTitle Vars === */ --title-color: var(--grid-fg); @@ -40,13 +41,14 @@ --divider-width: var(--size-12); --divider-height: var(--size-2); --divider-font: var(--font-mono); - --divider-font-size: var(--typo-size-2xl); + --divider-line-height: 1; + --divider-font-size: var(--typo-size-3xl); --divider-padding: 0 var(--typo-spacing-cozy); --subtitle-font: var(--font-mono); --subtitle-color: var(--grid-fg); --subtitle-font-size: var(--typo-size-xl); --subtitle-text-transform: uppercase; - --subtitle-letter-spacing: var(--typo-spacing-cozy); + --subtitle-letter-spacing: var(--typo-spacing-snug); --subtitle-transition: none; pointer-events: none; diff --git a/src/keystatic/collections/meta/article.ts b/src/keystatic/collections/meta/article.ts new file mode 100644 index 0000000..af64f39 --- /dev/null +++ b/src/keystatic/collections/meta/article.ts @@ -0,0 +1,13 @@ +import { collection } from '@keystatic/core'; + +import { createArticleField } from '@/keystatic/fields/article'; + +export default collection({ + label: 'Meta - Posts', + slugField: 'title', + path: 'content/meta/*', + format: { contentField: 'content' }, + schema: { + ...createArticleField('meta'), + }, +}); diff --git a/src/keystatic/collections/taxonomy/authors.ts b/src/keystatic/collections/taxonomy/authors.ts new file mode 100644 index 0000000..89098e8 --- /dev/null +++ b/src/keystatic/collections/taxonomy/authors.ts @@ -0,0 +1,46 @@ +import { collection, fields } from '@keystatic/core'; + +export default collection({ + label: 'Authors', + slugField: 'name', + path: 'content/taxonomy/authors/*', + format: { data: 'json' }, + schema: { + name: fields.slug({ name: { label: 'Name' } }), + avatar: fields.image({ + label: 'Avatar', + directory: 'public/images/authors', + publicPath: '/images/authors', + }), + description: fields.text({ label: 'Description', multiline: true }), + contacts: fields.array( + fields.object({ + type: fields.select({ + label: 'Contact Type', + defaultValue: 'email', + options: [ + { + value: 'email', + label: 'E-Mail', + }, + { + value: 'discord', + label: 'Discord', + }, + { + value: 'other', + label: 'Other', + }, + ], + }), + url: fields.text({ + label: 'URL', + }), + }), + { + label: 'Contact Type', + itemLabel: (props) => `${props.fields.type.value}`, + } + ), + }, +}); diff --git a/src/keystatic/collections/taxonomy/tags.ts b/src/keystatic/collections/taxonomy/tags.ts new file mode 100644 index 0000000..5ec4011 --- /dev/null +++ b/src/keystatic/collections/taxonomy/tags.ts @@ -0,0 +1,41 @@ +import { collection, fields } from '@keystatic/core'; + +export default collection({ + label: 'Tags', + slugField: 'name', + path: 'content/taxonomy/tags/*', + format: { data: 'json' }, + schema: { + name: fields.slug({ name: { label: 'Name' } }), + icon: fields.conditional( + fields.select({ + label: 'Icon Type', + defaultValue: 'none', + options: [ + { + value: 'none', + label: 'None', + }, + { + value: 'glyph', + label: 'Glyph', + }, + { + value: 'img', + label: 'Image', + }, + ], + }), + { + none: fields.empty(), + glyph: fields.text({ label: 'Glyph' }), + img: fields.image({ + label: 'icon', + publicPath: '/images/icons', + directory: 'public/images/icons', + }), + } + ), + description: fields.text({ label: 'Description', multiline: true }), + }, +}); diff --git a/src/keystatic/components/general/grid.ts b/src/keystatic/components/general/grid.ts new file mode 100644 index 0000000..b96a536 --- /dev/null +++ b/src/keystatic/components/general/grid.ts @@ -0,0 +1,61 @@ +import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon'; +import { fields } from '@keystatic/core'; +import { repeating, wrapper } from '@keystatic/core/content-components'; + +export const gridComponents = { + Row: repeating({ + label: 'Row', + icon: grid2X2Icon, + children: ['Column'], // This defines what can go inside + schema: { + style: fields.object({ + cols: fields.integer({ + label: 'Columns', + defaultValue: 2, + validation: { min: 2, max: 12 }, + }), + gap: fields.text({ + label: 'Gap', + description: 'Enter a variable or value', + }), + align: fields.select({ + label: 'Align Items', + defaultValue: 'flex-start', + options: [ + { label: 'Start', value: 'flex-start' }, + { label: 'End', value: 'flex-end' }, + { label: 'Center', value: 'center' }, + { label: 'Baseline', value: 'baseline' }, + { label: 'Stretch', value: 'stretch' }, + ], + }), + justify: fields.select({ + label: 'Justify Content', + defaultValue: 'flex-start', + options: [ + { label: 'Start', value: 'flex-start' }, + { label: 'End', value: 'flex-end' }, + { label: 'Center', value: 'center' }, + { label: 'Space Between', value: 'space-between' }, + { label: 'Space Around', value: 'space-around' }, + { label: 'Evenly', value: 'evenly' }, + { label: 'Stretch', value: 'stretch' }, + { label: 'Baseline', value: 'baseline' }, + ], + }), + }), + }, + }), + Column: wrapper({ + label: 'Column', + forSpecificLocations: true, + schema: { + style: fields.object({ + colspan: fields.integer({ + label: 'Colspan', + defaultValue: 1, + }), + }), + }, + }), +}; diff --git a/src/keystatic/components/general/index.ts b/src/keystatic/components/general/index.ts new file mode 100644 index 0000000..5010687 --- /dev/null +++ b/src/keystatic/components/general/index.ts @@ -0,0 +1,4 @@ +import { gridComponents } from '@/keystatic/components/general/grid'; +import { sidenoteComponents } from '@/keystatic/components/general/sidenote'; + +export const generalComponents = { ...gridComponents, ...sidenoteComponents }; diff --git a/src/keystatic/components/general/sidenote.ts b/src/keystatic/components/general/sidenote.ts new file mode 100644 index 0000000..0ef273e --- /dev/null +++ b/src/keystatic/components/general/sidenote.ts @@ -0,0 +1,59 @@ +import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon'; +import { fields } from '@keystatic/core'; +import { inline } from '@keystatic/core/content-components'; + +export const sidenoteComponents = { + Sidenote: inline({ + label: 'Sidenote', + icon: panelRightDashedIcon, + schema: { + id: fields.text({ + label: 'ID', + description: 'Unique Identifier (Auto-generated)', + }), + marker: fields.text({ + label: 'Marker', + description: 'Number or Glyph for reference', + defaultValue: '⋄', + validation: { + length: { + min: 1, + max: 3, + }, + }, + }), + content: fields.text({ + label: 'Note Content', + multiline: true, + validation: { + length: { + min: 1, + }, + }, + }), + type: fields.select({ + label: 'Type', + description: 'Visual Style', + options: [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Lore', + value: 'lore', + }, + { + label: 'Crunch', + value: 'crunch', + }, + { + label: 'Example', + value: 'example', + }, + ], + defaultValue: 'default', + }), + }, + }), +}; diff --git a/src/keystatic/fields/article.ts b/src/keystatic/fields/article.ts new file mode 100644 index 0000000..17ce3d3 --- /dev/null +++ b/src/keystatic/fields/article.ts @@ -0,0 +1,26 @@ +import { fields } from '@keystatic/core'; + +import { createContentField } from '@/keystatic/fields/content'; +import { createSEOField } from '@/keystatic/fields/seo'; +import { createMetaField } from '@/keystatic/fields/meta'; + +export const createArticleField = (imageSubfolder: string) => ({ + title: fields.slug({ name: { label: 'Title' } }), + summary: fields.text({ label: 'Summary', multiline: true }), + cover: fields.object({ + src: fields.image({ + label: 'Cover Image', + directory: `public/images/covers/${imageSubfolder}`, + publicPath: `/images/covers/${imageSubfolder}`, + }), + alt: fields.text({ label: 'Alt' }), + caption: fields.text({ label: 'Caption', multiline: true }), + showInHeader: fields.checkbox({ + label: 'Show in Header', + defaultValue: false, + }), + }), + meta: createMetaField(), + seo: createSEOField(), + content: createContentField(imageSubfolder), +}); diff --git a/src/keystatic/fields/content.ts b/src/keystatic/fields/content.ts new file mode 100644 index 0000000..3f6e652 --- /dev/null +++ b/src/keystatic/fields/content.ts @@ -0,0 +1,22 @@ +import { fields } from '@keystatic/core'; +import type { ContentComponent } from '@keystatic/core/content-components'; + +import { generalComponents } from '@/keystatic/components/general'; + +export const createContentField = ( + imageSubfolder: string, + additionalComponents?: Record<string, ContentComponent> +) => + fields.markdoc({ + label: 'Content', + options: { + image: { + directory: `public/images/content/${imageSubfolder}`, + publicPath: `/images/content/${imageSubfolder}`, + }, + }, + components: { + ...generalComponents, + ...additionalComponents, + }, + }); diff --git a/src/keystatic/fields/meta.ts b/src/keystatic/fields/meta.ts new file mode 100644 index 0000000..968b2ee --- /dev/null +++ b/src/keystatic/fields/meta.ts @@ -0,0 +1,44 @@ +import type { ComponentSchema } from '@keystatic/core'; +import { fields } from '@keystatic/core'; + +export const createMetaField = (): ComponentSchema => + fields.object( + { + publicationDate: fields.datetime({ + label: 'Publication Date', + defaultValue: { kind: 'now' }, + }), + updateDate: fields.datetime({ label: 'Update Date' }), + status: fields.select({ + label: 'Status', + defaultValue: 'draft', + options: [ + { label: 'Draft', value: 'draft' }, + { label: 'Published', value: 'published' }, + { label: 'Archived', value: 'archived' }, + ], + }), + isFeatured: fields.checkbox({ + label: 'Featured', + description: 'Show on Homepage', + }), + tags: fields.array( + fields.relationship({ + label: 'Tags', + collection: 'tags', + }), + { + label: 'Tags', + itemLabel: (props) => props.value || 'Select Tag', + } + ), + author: fields.relationship({ + label: 'Author', + collection: 'authors', + }), + }, + { + label: 'Meta Information', + layout: [4, 4, 4, 12, 12, 12], + } + ); diff --git a/src/keystatic/fields/seo.ts b/src/keystatic/fields/seo.ts new file mode 100644 index 0000000..5bf02b6 --- /dev/null +++ b/src/keystatic/fields/seo.ts @@ -0,0 +1,24 @@ +import type { ComponentSchema } from '@keystatic/core'; +import { fields } from '@keystatic/core'; + +export const createSEOField = (): ComponentSchema => + fields.object( + { + title: fields.text({ + label: 'SEO Title', + validation: { length: { max: 60 } }, + }), + description: fields.text({ + label: 'SEO Description', + multiline: true, + validation: { length: { max: 160 } }, + }), + noIndex: fields.checkbox({ + label: 'No Index', + description: 'Prevent search engines from indexing', + }), + }, + { + label: 'SEO Settings', + } + ); diff --git a/src/lib/markdoc/nodes.ts b/src/lib/markdoc/nodes.ts new file mode 100644 index 0000000..7a9f6cd --- /dev/null +++ b/src/lib/markdoc/nodes.ts @@ -0,0 +1,7 @@ +import { Config } from '@markdoc/markdoc'; + +export const nodes: Config['nodes'] = { + document: { + render: '', + }, +}; diff --git a/src/lib/markdoc/tags.ts b/src/lib/markdoc/tags.ts new file mode 100644 index 0000000..eb528af --- /dev/null +++ b/src/lib/markdoc/tags.ts @@ -0,0 +1,43 @@ +import { Config } from '@markdoc/markdoc'; + +export const tags: Config['tags'] = { + Row: { + render: 'Row', + attributes: { + style: { + type: 'Object', + required: true, + }, + }, + }, + Column: { + render: 'Column', + attributes: { + style: { + type: 'Object', + required: true, + }, + }, + }, + Sidenote: { + render: 'Sidenote', + attributes: { + id: { + type: 'String', + required: false, + }, + marker: { + type: 'String', + required: true, + }, + content: { + type: 'String', + required: true, + }, + type: { + type: 'String', + default: 'default', + }, + }, + }, +}; diff --git a/src/lib/markdoc/utils.ts b/src/lib/markdoc/utils.ts new file mode 100644 index 0000000..183e9dd --- /dev/null +++ b/src/lib/markdoc/utils.ts @@ -0,0 +1,34 @@ +import type { RenderableTreeNode } from '@markdoc/markdoc'; +import type { Sidenote } from '@/lib/types/components'; + +export function hasComponents( + renderable: RenderableTreeNode, + components: string[] +): boolean { + const renderablestring = JSON.stringify(renderable); + return components.some((component) => renderablestring.includes(component)); +} + +export function collectSideNotes( + node: RenderableTreeNode | RenderableTreeNode[] +): Sidenote[] { + const sidenotes: Sidenote[] = []; + + function walk(n: any) { + if (!n) return; + if (Array.isArray(n)) { + n.forEach(walk); + } else if (n.name === 'Sidenote' && n.attributes) { + sidenotes.push({ + id: n.attributes.id, + marker: n.attributes.marker, + content: n.attributes.content, + type: n.attributes.type || 'default', + }); + } + if (n.children) walk(n.children); + } + + walk(node); + return sidenotes; +} diff --git a/src/lib/next/generatePageMetaData.ts b/src/lib/next/generatePageMetaData.ts new file mode 100644 index 0000000..49ea443 --- /dev/null +++ b/src/lib/next/generatePageMetaData.ts @@ -0,0 +1,14 @@ +import { Metadata } from 'next'; +import { ArticleContent } from '@/lib/types/content'; + +export default function generatePageMetaData( + article: ArticleContent +): Metadata { + const seo = article.seo; + + return { + title: seo.title || article.title, + description: seo.description || article.summary, + robots: seo.noIndex, + }; +} diff --git a/src/lib/readers/meta/posts.ts b/src/lib/readers/meta/posts.ts new file mode 100644 index 0000000..8d86885 --- /dev/null +++ b/src/lib/readers/meta/posts.ts @@ -0,0 +1,15 @@ +import { cache, reader } from '@/lib/readers/base'; + +export const getMetaPosts = cache(async () => + reader.collections['meta_posts'].all() +); + +export const getMetaPostBySlug = cache(async (slug: string) => { + const article = await reader.collections['meta_posts'].read(slug); + return article ? { ...article, slug } : null; +}); + +export const getMetaHome = cache(async () => { + const article = await reader.collections['meta_posts'].read('index'); + return article ? { ...article } : null; +}); diff --git a/src/lib/readers/taxonomy/authors.ts b/src/lib/readers/taxonomy/authors.ts new file mode 100644 index 0000000..2f26a3e --- /dev/null +++ b/src/lib/readers/taxonomy/authors.ts @@ -0,0 +1,8 @@ +import { cache, reader } from '@/lib/readers/base'; + +export const getAuthors = cache(async () => reader.collections.authors.all()); + +export const getAuthorBySlug = cache(async (slug: string) => { + const author = await reader.collections.authors.read(slug); + return author ? { ...author, slug } : null; +}); diff --git a/src/lib/readers/taxonomy/tags.ts b/src/lib/readers/taxonomy/tags.ts new file mode 100644 index 0000000..0455683 --- /dev/null +++ b/src/lib/readers/taxonomy/tags.ts @@ -0,0 +1,8 @@ +import { cache, reader } from '@/lib/readers/base'; + +export const getTags = cache(async () => reader.collections.tags.all()); + +export const getTagBySlug = cache(async (slug: string) => { + const tag = await reader.collections.tags.read(slug); + return tag ? { ...tag, slug } : null; +}); diff --git a/src/lib/types/components.ts b/src/lib/types/components.ts new file mode 100644 index 0000000..abf246f --- /dev/null +++ b/src/lib/types/components.ts @@ -0,0 +1,6 @@ +export interface Sidenote { + id: string; + marker: string; + content: string; + type: 'default' | 'lore' | 'crunch' | 'example'; +} diff --git a/src/lib/types/content.ts b/src/lib/types/content.ts new file mode 100644 index 0000000..ad57706 --- /dev/null +++ b/src/lib/types/content.ts @@ -0,0 +1,3 @@ +import { MetaContent } from '@/lib/types/meta'; + +export type ArticleContent = MetaContent; diff --git a/src/lib/types/meta.ts b/src/lib/types/meta.ts new file mode 100644 index 0000000..448cdf8 --- /dev/null +++ b/src/lib/types/meta.ts @@ -0,0 +1,8 @@ +import { Entry } from '@keystatic/core/reader'; +import keystaticConfig from '~/keystatic.config'; + +export type MetaPost = Entry< + (typeof keystaticConfig.collections)['meta_posts'] +>; + +export type MetaContent = MetaPost; diff --git a/src/lib/types/taxonomy.ts b/src/lib/types/taxonomy.ts new file mode 100644 index 0000000..00265e5 --- /dev/null +++ b/src/lib/types/taxonomy.ts @@ -0,0 +1,8 @@ +import { Entry } from '@keystatic/core/reader'; +import keystaticConfig from '~/keystatic.config'; + +export type AuthorData = Entry<(typeof keystaticConfig.collections)['authors']>; +export type TagData = Entry<(typeof keystaticConfig.collections)['tags']>; + +export type Author = AuthorData & { slug: string }; +export type Tag = TagData & { slug: string }; diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts new file mode 100644 index 0000000..42b9dbf --- /dev/null +++ b/src/lib/utils/date.ts @@ -0,0 +1,24 @@ +export function toMilitaryDTG(datestring: string, timeZone = 'A') { + const date = new Date(datestring); + const day = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const months = [ + 'JAN', + 'FEB', + 'MAR', + 'APR', + 'MAY', + 'JUN', + 'JUL', + 'AUG', + 'SEP', + 'OCT', + 'NOV', + 'DEC', + ]; + const month = months[date.getUTCMonth()]; + const year = date.getUTCFullYear().toString().slice(-2); + + return `${day}${hours}${minutes}${timeZone}${month}${year}`; +} diff --git a/src/lib/utils/guards.ts b/src/lib/utils/guards.ts new file mode 100644 index 0000000..be2bff0 --- /dev/null +++ b/src/lib/utils/guards.ts @@ -0,0 +1,13 @@ +export function isValidCover(cover: { + src: string | null; + alt: string; + caption: string; + showInHeader: boolean; +}): cover is { + src: string; + alt: string; + caption: string; + showInHeader: boolean; +} { + return cover.showInHeader && cover.src !== null; +} diff --git a/src/styles/globals/content.css b/src/styles/globals/content.css index 3911e3b..43b768e 100644 --- a/src/styles/globals/content.css +++ b/src/styles/globals/content.css @@ -1,5 +1,7 @@ @layer content { .content { + font-family: var(--font-body); + & h1 { margin-block: var(--el-h1-vspace-top) var(--el-h1-vspace-bottom); padding-bottom: var(--spacing-snug); @@ -430,19 +432,21 @@ } a { - @mixin anim-txt-pixelshift 0.6s steps(15, end), var(--color-primary); + &:not([class]) { + @mixin anim-txt-typewriter; - color: var(--color-text-tertiary); - transition: color 0.2s ease-in-out; + color: var(--color-text-tertiary); + transition: color 0.2s ease-in-out; - &:hover { - cursor: pointer; - color: var(--color-primary); - } + &:hover { + cursor: pointer; + color: var(--color-primary); + } - &:visited, - &:active { - color: var(--color-primary-emphasis); + &:visited, + &:active { + color: var(--color-primary-emphasis); + } } } diff --git a/src/styles/globals/custom-media.css b/src/styles/globals/custom-media.css index cddf695..06e6a60 100644 --- a/src/styles/globals/custom-media.css +++ b/src/styles/globals/custom-media.css @@ -4,4 +4,6 @@ @custom-media --bp-tablet-up (width >= 48rem); @custom-media --bp-tablet-down (width < 80rem); @custom-media --bp-desktop (width >= 80rem); + @custom-media --max-layout (width <= 92rem); + @custom-media --bp-margin (width >= 64rem); } \ No newline at end of file diff --git a/src/styles/globals/dimensions.css b/src/styles/globals/dimensions.css index c9d69b8..b53385c 100644 --- a/src/styles/globals/dimensions.css +++ b/src/styles/globals/dimensions.css @@ -29,8 +29,8 @@ --size-192: 48rem; --size-256: 64rem; --size-320: 80rem; - --size-384: 96rem; --size-360: 90rem; + --size-384: 96rem; --size-400: 100rem; --size-480: 120rem; @@ -91,5 +91,9 @@ --spacing-generous: var(--size-16); --spacing-luxurious: var(--size-24); --spacing-expansive: var(--size-32); + + /* == Responsive Content Dimensions */ + --layout-max-width: 1280px; + --content-max-width: 75ch; } } \ No newline at end of file diff --git a/src/styles/globals/typography.css b/src/styles/globals/typography.css index dd245eb..fbc99bc 100644 --- a/src/styles/globals/typography.css +++ b/src/styles/globals/typography.css @@ -9,7 +9,7 @@ --font-mono: var(--font-iosevka-mono); /* == Font Sizes == */ - --typo-size-responsive: clamp(1rem, 2.5vw, 1.25rem); + --typo-size-responsive: clamp(1rem, 2.5vw, 1.5rem); --typo-size-base: 16px; --typo-size-8xl: 8em; --typo-size-7xl: 6.375em; @@ -68,6 +68,5 @@ --vspace-comfortable: 1.75; --vspace-loose: 2; --vspace-spacious: 2.5; - } } \ No newline at end of file diff --git a/src/styles/mixins/containers.css b/src/styles/mixins/containers.css index 20e4145..1f5e6b7 100644 --- a/src/styles/mixins/containers.css +++ b/src/styles/mixins/containers.css @@ -5,4 +5,16 @@ padding: $vspacing $hspacing; font-family: var(--font-body); font-size: $fontSize; +} + +@define-mixin layout-wrapper{ + @mixin mx auto; + + width: 100%; + max-width: var(--layout-max-width); + + @media screen and (--max-layout) { + @mixin px var(--spacing-comfortable); + } + } \ No newline at end of file