Compare commits
13 Commits
feature/ui
...
feature/ar
| Author | SHA1 | Date | |
|---|---|---|---|
| ce885d8eae | |||
| 0bca3573f7 | |||
| b93f846156 | |||
| 5cb4bd5782 | |||
| 3a79f59f03 | |||
| 70e226057a | |||
| 8f78c26b78 | |||
| e4b72005f2 | |||
| 2e30b1d6ed | |||
| ae6da529cf | |||
| fdcfc774fb | |||
| 054d450273 | |||
| 0111cd71fe |
86
content/awq/articles/anatomy-of-a-player-character.mdoc
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
title: Anatomy of a Player Character
|
||||||
|
path: awq
|
||||||
|
cover:
|
||||||
|
showInHeader: false
|
||||||
|
meta:
|
||||||
|
publicationDate: 2025-10-08T08:43:00.000Z
|
||||||
|
status: draft
|
||||||
|
isFeatured: false
|
||||||
|
tags: []
|
||||||
|
seo:
|
||||||
|
noIndex: false
|
||||||
|
---
|
||||||
|
Every model shares some common abilities
|
||||||
|
|
||||||
|
## Main Characteristics
|
||||||
|
Characteristics define the innate abilities of a model, and each characteristic has two skills associated with it
|
||||||
|
- **Weapon Skill (WS):** Aptitude in landing and avoiding blows in close combat; governs _Melee_ and _Defence_
|
||||||
|
- **Ballistic Skill (BS):** Capability of Hand-Eye-Coordination; governs _Skirmish_ and _Evasion_
|
||||||
|
- **Strength (S):** Used for brute force, stamina and might; governs _Brawn_ and _Toil_
|
||||||
|
- **Toughness (T):** Resistance to physical Trauma; governs _Consume Alcohol_ and _Endurance_
|
||||||
|
- **Initiative(I):** Reflects speed of thought and perception; governs _Perception_ and _Outdoor Survival_
|
||||||
|
- **Dexterity (Dex):** Affinity for performing fine and delicate manual tasks; governs _Skullduggery_ and _Tradecraft_
|
||||||
|
- **Agility (Ag):** Physical coordination and natural athleticism; governs _Stealth_ and _Athletics_
|
||||||
|
- **Intelligence (Int):** Power of thought, analysis, and understanding; governs _Intuition_ and _Education_
|
||||||
|
- **Willpower (WP):** General strength if mind; governs _Cool_ and _Animal Handling_
|
||||||
|
- **Fellowship (Fel):** Ability to get on with people; governs _Leadership_ and _Charm_
|
||||||
|
|
||||||
|
## Secondary Characteristics
|
||||||
|
- **Corruption Threshold (CT):[^*]** Ability to withstand _Mutation_
|
||||||
|
- **Insanity Threshold (IT):[^*]** Ability to withstand _Insanity_
|
||||||
|
- **Wounds (W):** Number of wounds a model can endure before _out of play_
|
||||||
|
- **Movement (M):** Indicator of how far a model can move under normal conditionsA
|
||||||
|
- **Fate Points (FP):[^*]** Can be used to avoid certain death and dark fates
|
||||||
|
- **Luck Points (LP):[^*]** Used for Re-rolls
|
||||||
|
- **Attacks (A):** Indicator of the numbers of attacks a character can make in a single round
|
||||||
|
- **Magic (Mag):** Denotes the Model's Wizard Level
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
Used in _Checks_
|
||||||
|
- **Melee (WS):** Used to make _Melee Attack_ actions
|
||||||
|
- **Defense (WS):** Used to defend against a model's _Melee Attack_ action
|
||||||
|
- **Shooting (BS):** Used to make _Ranged Attack_ actions with _Missile_ weapons
|
||||||
|
- **Throwing (BS):** Used to make _Ranged Attack_ actions with _Throwing_ weapons
|
||||||
|
- **Brawn (S):** Used for immediate feats of strength
|
||||||
|
- **Toil (T):** Used for prolonged manual labor
|
||||||
|
- **Consume Alcohol (T):** Used for resisting short-term hazards like alcohol and poison
|
||||||
|
- **Endurance (T):** Used to endure hardship, withstand deprivation, and survive harsh environments
|
||||||
|
- **Perception (I):** Used to notice things
|
||||||
|
- **Dodge (I):** Used to evade attacks and immediate hazards, e.g. Traps
|
||||||
|
- **Stealth (Ag):** Used for moving quietly and concealing
|
||||||
|
- **Athletics (Ag):** Used for running, jumping, climbing, and swimming
|
||||||
|
- **Streetwise (Dex):** Used for picking locks or pockets, disarming traps and other feats of Sleight of Hand
|
||||||
|
- **Crafting (Dex):** Used when crafting trappings
|
||||||
|
- **Intuition (Int):** Used for detecting subterfuge and determining value of objects
|
||||||
|
- **Education (Int):** Used for recalling relevant information
|
||||||
|
- **Cool (WP):** Used to remain calm under stress, resisting fear, and psychological coercion
|
||||||
|
- **Animal Handling (WP):** Used to charm, train, and care for animals
|
||||||
|
- **Leadership (Fel):** Used for intimidation, command, and coercing obedience; often resisted by _Discipline_
|
||||||
|
- **Charm (Fel):** Used for deceiving, blathering, haggling, gossiping; usually resisted by _Intuition_
|
||||||
|
|
||||||
|
## Traits
|
||||||
|
Inherent abilities often based on Species, examples include Flier (for flying Monsters), Dark Vision, Mutation, and so on
|
||||||
|
|
||||||
|
## Lore[^*]
|
||||||
|
Define what a character knows and specialist skills and come in the following categories
|
||||||
|
- _Academic:_ Represent various academic fields like Accountancy, Anatomy, History and Law
|
||||||
|
- _Cultural:_ Represent the knowledge of social groups and language
|
||||||
|
- _Enemy:_ Represent the knowledge of adversaries and how to effectively combat them
|
||||||
|
- _Environment:_ Represent the knowledge of surviving in various hazardous environment
|
||||||
|
- _Magic Lores:_ Represent the knowledge of the various forms of magic and enables casting spells from them+
|
||||||
|
- _Specialist Weapon Groups:_ Represent training with special weapons like Polearms, Two-Handed Swords, Blackpowder Guns
|
||||||
|
- _Trade Lores:_ Represent the knowledge and ability to create certain trappings or work in certain fields, example Blacksmith, Weaver, Engineer, Artist
|
||||||
|
- _Vehicle Lores:_ Represent knowledge in operating vehicles and mounts
|
||||||
|
|
||||||
|
## Talents[^*]
|
||||||
|
Represent certain knacks, tricks and innate abilities a character has, examples would be Ambidextrous, Aetheric Attunement, Menacing or Acute Senses
|
||||||
|
|
||||||
|
## Maneuvers[^*]
|
||||||
|
Represent special combat actions and tactics a character can employ during combat, examples include Formation Fighting, Disarm, and Shield Bash
|
||||||
|
|
||||||
|
## Careers[^*]
|
||||||
|
Represent building blocks of a character, that provide certain skills, lores, talents, maneuvers, etc.
|
||||||
|
|
||||||
|
|
||||||
|
[^*]: Only applicable for _Player Characters_
|
||||||
139
content/meta/index.mdoc
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
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 the wailings
|
||||||
|
of their wenches
|
||||||
|
showInHeader: true
|
||||||
|
meta:
|
||||||
|
publicationDate: 2025-10-01T10:30:00.000Z
|
||||||
|
status: draft
|
||||||
|
isFeatured: false
|
||||||
|
tags:
|
||||||
|
- how-and-what
|
||||||
|
author: dave-damage
|
||||||
|
seo:
|
||||||
|
noIndex: false
|
||||||
|
---
|
||||||
|
Et Pater tuus, qui videt in abscondito, reddet tibi. Et cum oratis, non eritis sicut hypocritæ qui amant in synagogis et in angulis platearum stantes orare, ut videantur ab hominibus amen dico vobis, receperunt mercedem suam. Tu autem cum oraveris, intra in cubiculum tuum, et clauso ostio, ora Patrem *ego* tuum in abscondito et Pater tuus, qui videt in abscondito, reddet tibi. Orantes autem, nolite multum loqui, sicut ethnici, putant enim quod in multiloquio suo exaudiantur. Nolite ergo assimilari eis scit enim Pater vester, quid opus sit vobis, antequam petatis eum. Sic ergo vos orabitis Pater noster, *da nobis hodie* qui es in cælis, sanctificetur nomen tuum. Adveniat regnum tuum; fiat voluntas tua, sicut in cælo et in terra. Panem nostrum supersubstantialem da nobis hodie, et dimitte nobis debita nostra, sicut et nos dimittimus debitoribus nostris. Et ne nos inducas in tentationem, sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater *piscem petierit* vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ.
|
||||||
|
|
||||||
|
{% Callout type="spoiler" title="I HAVE A TITLE! YAY" %}
|
||||||
|
Ubi fures effodiunt, et furantur. Thesaurizate autem vobis thesauros in cælo, ubi neque ærugo, neque tinea demolitur, et ubi fures non effodiunt, nec furantur. Ubi enim est thesaurus tuus, ibi est et cor tuum. Lucerna corporis tui est oculus tuus. Si oculus tuus fuerit simplex, totum corpus tuum lucidum erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro *ex gypto vocavi filium* quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite volatilia cæli, quoniam non serunt, neque metunt, neque congregant in horrea et Pater vester cælestis pascit illa. Nonne vos magis pluris estis illis? Quis autem vestrum cogitans potest adjicere ad *per isaiam prophetam dicentem* staturam suam cubitum *modic fidei tunc surgens* unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt.
|
||||||
|
{% /Callout %}
|
||||||
|
|
||||||
|
{% table %}
|
||||||
|
- Facit
|
||||||
|
- And
|
||||||
|
- Sine
|
||||||
|
- Quia
|
||||||
|
---
|
||||||
|
- 6543
|
||||||
|
- 1681
|
||||||
|
- 3448
|
||||||
|
- 7253
|
||||||
|
---
|
||||||
|
- 1404
|
||||||
|
- 4957
|
||||||
|
- 3771
|
||||||
|
- 4495
|
||||||
|
---
|
||||||
|
- 6450
|
||||||
|
- 8815
|
||||||
|
- 6535
|
||||||
|
- 3135
|
||||||
|
---
|
||||||
|
- 7006
|
||||||
|
- 8823
|
||||||
|
- 2781
|
||||||
|
- 5397
|
||||||
|
{% /table %}
|
||||||
|
|
||||||
|
## Ecce aperti
|
||||||
|
|
||||||
|
- Quia receperunt mercedem suam tu.
|
||||||
|
- Small accipit and qurit invenit pulsanti and aperietur.
|
||||||
|
- Ascendit in montem et cum.
|
||||||
|
- Occiderit reus erit `Erit. Si autem oculus` judicio [Numquid lapidem porriget ei](https://example.com/ingypt/fratr) ego autem dico.
|
||||||
|
- Ex istis si **venti et mare obediunt** autem fnum **baptizo vos in aqua** agri.
|
||||||
|
- Suas ut appareant [Justitiam tunc `Dicens` dimisit](https://example.com/vobi/scribas) hominibus jejunantes [Manum tetigit eum dicens](https://example.com/regn/est) amen dico.
|
||||||
|
- Spiritu sancto et igni cujus.
|
||||||
|
- Triticum suum in horreum *vocavi* paleas autem comburet.
|
||||||
|
- Abiit opinio ejus in totam
|
||||||
|
- His omnibus indigetis qurite ergo primum regnum
|
||||||
|
|
||||||
|
Puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent cum Abraham, et Isaac, et Jacob in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et surrexit, et ministrabat eis. Vespere autem facto, obtulerunt ei multos dæmonia habentes et ejiciebat spiritus verbo, et omnes male habentes curavit. Ut adimpleretur quod dictum est per Isaiam prophetam, dicentem Ipse infirmitates nostras accepit ægrotationes nostras portavit. Videns autem Jesus turbas multas circum se.
|
||||||
|
|
||||||
|
{% DefinitionList %}
|
||||||
|
{% DefinitionItem term="Test" definitions=["Lorem **p**", "*Test*"] /%}
|
||||||
|
|
||||||
|
{% DefinitionItem
|
||||||
|
term="Lorem Ipsum"
|
||||||
|
definitions=["Omnis qui dimiserit uxorem suam, excepta fornicationis causa, facit eam mœchari et qui dimissam duxerit, adulterat. Iterum audistis quia dictum est antiquis Non perjurabis reddes autem Domino juramenta tua. Ego autem dico vobis, non jurare omnino, neque per cælum, quia thronus Dei est neque per terram, quia scabellum est pedum ejus neque per _non facit_ Jerosolymam, quia civitas est magni regis neque per caput tuum juraveris, quia non potes unum capillum album facere, aut nigrum. Sit _et_ autem sermo vester, est, est non, non."] /%}
|
||||||
|
{% /DefinitionList %}
|
||||||
|
|
||||||
|
### Your ad in
|
||||||
|
|
||||||
|
Erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite volatilia cæli, quoniam non serunt, neque metunt, neque congregant in horrea et Pater vester cælestis pascit illa. Nonne vos magis pluris estis illis? Quis autem vestrum{% Sidenote #makrum marker="⌀" content="Sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ, tristes. Exterminant enim facies suas, ut appareant hominibus jejunantes. Amen dico vobis, quia receperunt mercedem **autem jesus** suam. Tu autem, cum jejunas, unge caput tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater **dicit eis jesus** tuus, qui videt in abscondito, reddet tibi. Nolite thesaurizare vobis thesauros in." type="default" /%} 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.
|
||||||
|
|
||||||
|
{% Blockquote
|
||||||
|
quote="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 _galil vidit duos_ persecuti sunt prophetas, qui fuerunt ante vos. Vos estis sal terræ. Quod si sal **zonam pelliceam** evanuerit, in quo **extendens** salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. _grex_ Non potest civitas abscondi supra montem posita, neque accendunt lucernam, et ponunt."
|
||||||
|
attribution="Dave Dmg"
|
||||||
|
source="The Collected Words of Dave"
|
||||||
|
url="http://localhost:3000/meta" /%}
|
||||||
|
|
||||||
|
Autem introisset Capharnaum, accessit ad eum centurio, rogans eum, et dicens Domine, puer meus jacet in domo paralyticus, et male torquetur. Et ait illi Jesus Ego veniam, et curabo eum. Et respondens centurio, ait Domine, non sum dignus ut intres sub tectum meum sed tantum dic verbo, et sanabitur puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo [Ad eum discipuli](https://example.com/suamt/andi) Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent [Qui dictus est](https://example.com/autem/etmalo) cum Abraham, et Isaac, et ==facies suas== Jacob [Modic fidei tunc surgens imperavit ventis](https://example.com/eating/adimple) in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et.
|
||||||
|
|
||||||
|
{% Accordion %}
|
||||||
|
{% AccordionItem title="Lorem" defaultOpen=false %}
|
||||||
|
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 aut**em 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 ergo primum regnum Dei, et justitiam *aliam* ejus et hæc omnia adjicientur vobis. Nolite ergo *in via cum* solliciti.
|
||||||
|
{% /AccordionItem %}
|
||||||
|
{% /Accordion %}
|
||||||
|
|
||||||
|
#### Ubi christus nasceretur at illi dixerunt in bethlehem
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def hello_world
|
||||||
|
puts "Hello, World!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Judæ sic enim scriptum est per prophetam Tunc Herodes clam vocatis magis diligenter didicit ab eis tempus stellæ, quæ apparuit eis et mittens illos in Bethlehem, dixit Ite, et interrogate diligenter de puero et cum inveneritis, renuntiate mihi, ut et ego veniens adorem eum. Qui cum audissent regem, abierunt, et ecce stella, quam viderant in oriente, antecedebat eos, usque dum veniens staret supra, ubi erat puer. Videntes autem stellam gavisi sunt gaudio magno valde. Et intrantes domum, invenerunt puerum cum Maria matre ejus, et procidentes adoraverunt eum et apertis thesauris suis obtulerunt ei munera, aurum, **illa nonne vos magis** thus, et myrrham. Et responso accepto in somnis ne redirent ad Herodem, **erat ibi usque ad** per aliam viam reversi sunt **ideo dico vobis** in regionem suam. Qui cum recessissent, ecce angelus Domini apparuit in somnis Joseph, dicens Surge, et accipe puerum, et matrem ejus, et fuge in Ægyptum, et esto ibi usque dum dicam tibi. Futurum est enim ut Herodes quærat puerum ad perdendum eum. Qui consurgens accepit puerum et matrem ejus nocte, et secessit in.
|
||||||
|
|
||||||
|
{% Figure
|
||||||
|
src="/images/figures/index/pexels-clickerhappy-534590.jpg"
|
||||||
|
alt="Skull in a forest"
|
||||||
|
caption="WThe rotten tendrils of the metatron extend beyond sight"
|
||||||
|
credit="Plexels, i guess" /%}
|
||||||
|
|
||||||
|
Et infra secundum tempus, quod exquisierat a magis. Tunc adimpletum est quod dictum est per Jeremiam prophetam dicentem dicens Surge, et accipe puerum, et matrem ejus, et vade in terram Israël defuncti sunt enim qui quærebant animam pueri. Qui consurgens, accepit puerum, et matrem ejus, et venit in terram Israël. Audiens autem quod Archelaus regnaret in Judæa 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 per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes 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 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, et omnis regio circa Jordanem; et baptizabantur ab eo in Jordane, confitentes peccata sua. Videns autem multos pharisæorum, et sadducæorum, venientes.
|
||||||
|
|
||||||
|
##### Abiit totus grex per prceps in
|
||||||
|
|
||||||
|
Tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater tuus, qui videt in abscondito, reddet tibi. Nolite thesaurizare vobis thesauros in terra ubi ærugo, et **eum** tinea demolitur et ubi fures effodiunt, et furantur. Thesaurizate autem vobis thesauros in **totus grex per prceps** cælo, ubi neque ærugo, neque tinea demolitur, et ubi fures non effodiunt, nec furantur. Ubi enim est thesaurus tuus, ibi est et cor tuum. Lucerna corporis tui est oculus tuus. Si oculus tuus fuerit simplex, totum corpus **sancto et** tuum lucidum erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite.
|
||||||
|
|
||||||
|
1. Invenit pulsanti and aperietur aut quis.
|
||||||
|
1. Alia duo qui petit.
|
||||||
|
1. Puer videntes autem `Da ei et volenti mutuari` stellam gavisi.
|
||||||
|
1. Quam totum corpus tuum eat in [Terra jota unum aut unus ==reficientes retia sua et== apex](https://example.com/filios/eumq) gehennam.
|
||||||
|
1. Et faciem ==that== tuam lava.
|
||||||
|
1. Matrem ejus et venit in
|
||||||
|
1. Oriente venerunt jerosolymam dicentes.
|
||||||
|
1. Magis diligenter didicit ==pater vester clestis== ab eis.
|
||||||
|
1. Numquid serpentem porriget ei **quod si sal evanuerit** if *longe* your.
|
||||||
|
1. Viderant in oriente antecedebat eos [Maria matre ejus et](https://example.com/munust/insom) *ecce* usque.
|
||||||
|
|
||||||
|
###### Et vocavit eos illi
|
||||||
|
|
||||||
|
Meus dilectus, in quo mihi complacui. Tunc Jesus ductus est in desertum a Spiritu, ut tentaretur a diabolo. Et cum jejunasset quadraginta diebus, et quadraginta noctibus, postea esuriit. Et accedens tentator dixit ei Si Filius Dei es, dic ut lapides isti panes fiant. Qui respondens dixit Scriptum est Non in solo pane vivit homo, sed in omni verbo, quod procedit de ore Dei. Tunc assumpsit eum diabolus in sanctam civitatem, et statuit eum super pinnaculum templi, et dixit ei Si Filius Dei es, mitte te deorsum. Scriptum est enim Quia angelis suis mandavit de te, et in manibus tollent te, ne forte offendas ad lapidem pedem tuum. Ait illi Jesus Rursum scriptum est Non tentabis Dominum Deum tuum. Iterum assumpsit eum diabolus in montem excelsum valde et ostendit ei omnia regna mundi, et gloriam eorum, et dixit ei Hæc omnia tibi dabo, si cadens adoraveris me. Tunc dicit ei Jesus Vade Satana Scriptum est enim Dominum Deum tuum adorabis, et illi soli servies. Tunc reliquit eum diabolus et ecce angeli accesserunt, et ministrabant ei. Cum autem audisset Jesus quod Joannes traditus esset, secessit in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et.
|
||||||
|
|
||||||
|
Tibi. Orantes autem, nolite multum loqui, sicut ethnici, putant enim quod in multiloquio suo exaudiantur. Nolite ergo assimilari eis scit enim Pater vester, quid opus sit vobis, antequam petatis eum. Sic ergo vos orabitis Pater noster, qui es in cælis, sanctificetur nomen tuum. Adveniat regnum tuum; fiat voluntas tua, sicut in cælo et in terra. Panem nostrum supersubstantialem da nobis hodie, et dimitte nobis debita nostra, sicut et nos dimittimus debitoribus nostris. Et ne nos inducas in tentationem, sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ, tristes. Exterminant enim facies suas, ut appareant hominibus jejunantes. Amen dico vobis, quia receperunt mercedem suam. Tu autem, cum jejunas, unge caput tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater tuus, qui videt.
|
||||||
|
|
||||||
|
Soli servies. Tunc reliquit eum diabolus et ecce angeli accesserunt, et ministrabant ei. Cum autem audisset Jesus quod Joannes traditus esset, secessit *ejus ipse* in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et Nephthalim ut adimpleretur quod dictum est per Isaiam prophetam Terra Zabulon, et terra **dictum est per prophetas** Nephthalim, via maris trans Jordanem, alilæa ==in== gentium populus, qui sedebat in tenebris, vidit lucem magnam et sedentibus in regione **adveniat regnum tuum fiat** umbræ mortis, lux orta est eis. Exinde cœpit Jesus prædicare, et dicere Pœnitentiam agite appropinquavit enim regnum cælorum. Ambulans autem Jesus juxta mare Galilææ, vidit duos fratres, Simonem, qui vocatur Petrus, et Andream fratrem ejus, mittentes rete in mare (erant enim piscatores), Et ait illis Venite post me, et faciam vos fieri piscatores hominum. At illi continuo relictis retibus secuti sunt eum. Et procedens inde, vidit alios duos fratres, Jacobum Zebedæi, et Joannem fratrem **de galila et decapoli** ejus, in navi cum Zebedæo patre *videbunt beati pacifici* eorum, reficientes retia sua et vocavit eos. Illi autem statim relictis retibus et patre, secuti sunt eum. Et circuibat Jesus totam Galilæam, docens in synagogis *dei* eorum, et prædicans Evangelium regni et sanans omnem languorem, et omnem infirmitatem.
|
||||||
|
|
||||||
|
Quod Joannes traditus esset, secessit in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et Nephthalim ut adimpleretur quod dictum est per Isaiam prophetam Terra Zabulon, et terra Nephthalim, via maris trans Jordanem, alilæa gentium populus, qui sedebat in tenebris, vidit lucem magnam et sedentibus in regione umbræ mortis, lux orta est eis. Exinde cœpit Jesus prædicare, et dicere Pœnitentiam agite appropinquavit enim regnum cælorum. Ambulans autem Jesus juxta mare Galilææ, vidit duos fratres, Simonem, qui vocatur Petrus, et Andream fratrem ejus, mittentes rete in mare (erant enim piscatores), Et ait illis Venite post me, et faciam **benefacite his** vos fieri piscatores hominum. At illi continuo relictis retibus secuti sunt eum. Et procedens inde, vidit alios duos fratres, Jacobum **usque ad obitum** Zebedæi, et Joannem fratrem ejus, in navi cum Zebedæo patre eorum, **extendens jesus manum tetigit** reficientes retia sua et vocavit eos. Illi autem statim relictis retibus et patre, secuti sunt eum. Et circuibat Jesus totam Galilæam, docens.
|
||||||
|
|
||||||
|
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 quidem baptizo vos in aqua in pœnitentiam qui autem post me venturus est, fortior me est, cujus non sum dignus calceamenta portare ipse vos baptizabit in Spiritu Sancto, et igni. Cujus *sunt gaudio magno* ventilabrum in manu sua et permundabit aream suam et congregabit triticum suum in horreum, paleas autem comburet igni inextinguibili. Tunc venit Jesus a Galilæa in Jordanem ad Joannem, ut baptizaretur ab eo. Joannes autem prohibebat eum, dicens Ego a te debeo baptizari, et tu venis ad me? Respondens autem Jesus, dixit ei Sine modo sic enim decet nos implere omnem justitiam. Tunc dimisit eum. Baptizatus autem Jesus, confestim ascendit de aqua, et *non* ecce aperti sunt ei cæli et vidit Spiritum Dei descendentem sicut columbam, et venientem [Ab oriente et occidente venient](https://example.com/eccelep/ergoass) super se. Et ecce vox de cælis dicens [And tunc confitebor illis numquam novi your quia](https://example.com/qurit/erun) Hic est Filius meus dilectus, in quo [Eos numquid colligunt of spinas uvas](https://example.com/etob/dixit) *homines mirati sunt dicentes* mihi complacui. Tunc Jesus ductus est.
|
||||||
|
|
||||||
|
Tuum, et odio habebis inimicum tuum. Ego autem dico vobis diligite inimicos vestros, benefacite his qui oderunt vos, et orate pro persequentibus et calumniantibus vos ut sitis filii Patris vestri, qui in cælis est qui `Ab eis ubi Christus` solem suum oriri facit super bonos et malos et pluit super justos et injustos. Si enim diligitis eos qui vos diligunt, quam mercedem habebitis? nonne et publicani hoc faciunt? Et si salutaveritis fratres vestros tantum, quid amplius facitis? nonne et ethnici hoc faciunt? Estote ergo vos perfecti, sicut et Pater vester cælestis perfectus est. Attendite ne justitiam vestram faciatis coram hominibus, ut videamini ab eis alioquin mercedem non habebitis apud Patrem vestrum qui in cælis est. Cum ergo facis eleemosynam, noli tuba canere ante te, sicut hypocritæ faciunt in synagogis, et in vicis, ut honorificentur ab hominibus. Amen dico vobis, receperunt mercedem suam. Te autem faciente eleemosynam, nesciat sinistra tua quid faciat dextera tua ut sit eleemosyna tua in abscondito, et Pater tuus, qui videt in abscondito, reddet tibi. Et cum oratis, non eritis sicut hypocritæ qui amant in synagogis et in.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Kitchensink",
|
"name": "Kitchensink",
|
||||||
"path": "/",
|
"path": "/kitchensink",
|
||||||
"gridPosition": "area_2",
|
"gridPosition": "area_2",
|
||||||
"variant": "kitchensink",
|
"variant": "kitchensink",
|
||||||
"sublinks": {
|
"sublinks": {
|
||||||
@@ -123,17 +123,17 @@
|
|||||||
"path": "/chainbreaker",
|
"path": "/chainbreaker",
|
||||||
"gridPosition": "area_5",
|
"gridPosition": "area_5",
|
||||||
"variant": "chainbreaker",
|
"variant": "chainbreaker",
|
||||||
"background": "/images/categories/items/4/background.png",
|
"background": "/images/categories/items/4/background.jpg",
|
||||||
"sublinks": {
|
"sublinks": {
|
||||||
"discriminant": false
|
"discriminant": false
|
||||||
},
|
},
|
||||||
"subtitle": {
|
"subtitle": {
|
||||||
"discriminant": true,
|
"discriminant": true,
|
||||||
"value": {
|
"value": {
|
||||||
"content": "Spear & Animism",
|
"content": "A history of Violence",
|
||||||
"divider": {
|
"divider": {
|
||||||
"discriminant": true,
|
"discriminant": true,
|
||||||
"value": "⎊"
|
"value": "⨳"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
content/taxonomy/authors/dave-damage.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
8
content/taxonomy/tags/how-and-what.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Was ist Was?",
|
||||||
|
"icon": {
|
||||||
|
"discriminant": "glyph",
|
||||||
|
"value": "‽"
|
||||||
|
},
|
||||||
|
"description": "General information and introductions for stuff"
|
||||||
|
}
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
import { config } from '@keystatic/core';
|
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';
|
||||||
|
|
||||||
|
import AWQColletions from '@/keystatic/collections/awq';
|
||||||
|
import awqCollections from '@/keystatic/collections/awq';
|
||||||
|
|
||||||
export default config({
|
export default config({
|
||||||
storage: {
|
storage: {
|
||||||
kind: 'local',
|
kind: 'local',
|
||||||
},
|
},
|
||||||
collections: {},
|
singletons: { navigation: NavSingleton },
|
||||||
singletons: { navigation },
|
collections: {
|
||||||
|
authors: AuthorsCollection,
|
||||||
|
tags: TagsCollection,
|
||||||
|
meta_posts: MetaPostsCollection,
|
||||||
|
...awqCollections,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@keystar/ui": "^0.7.19",
|
||||||
"@keystatic/core": "^0.5.48",
|
"@keystatic/core": "^0.5.48",
|
||||||
"@keystatic/next": "^5.0.4",
|
"@keystatic/next": "^5.0.4",
|
||||||
"@markdoc/markdoc": "^0.5.4",
|
"@markdoc/markdoc": "^0.5.4",
|
||||||
|
"marked": "^16.3.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0"
|
||||||
|
|||||||
13
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
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':
|
'@keystatic/core':
|
||||||
specifier: ^0.5.48
|
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)
|
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)
|
||||||
@@ -17,6 +20,9 @@ importers:
|
|||||||
'@markdoc/markdoc':
|
'@markdoc/markdoc':
|
||||||
specifier: ^0.5.4
|
specifier: ^0.5.4
|
||||||
version: 0.5.4(@types/react@19.1.13)(react@19.1.0)
|
version: 0.5.4(@types/react@19.1.13)(react@19.1.0)
|
||||||
|
marked:
|
||||||
|
specifier: ^16.3.0
|
||||||
|
version: 16.3.0
|
||||||
next:
|
next:
|
||||||
specifier: 15.5.3
|
specifier: 15.5.3
|
||||||
version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -2925,6 +2931,11 @@ packages:
|
|||||||
markdown-table@3.0.4:
|
markdown-table@3.0.4:
|
||||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||||
|
|
||||||
|
marked@16.3.0:
|
||||||
|
resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==}
|
||||||
|
engines: {node: '>= 20'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
match-sorter@6.3.4:
|
match-sorter@6.3.4:
|
||||||
resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==}
|
resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==}
|
||||||
|
|
||||||
@@ -7827,6 +7838,8 @@ snapshots:
|
|||||||
|
|
||||||
markdown-table@3.0.4: {}
|
markdown-table@3.0.4: {}
|
||||||
|
|
||||||
|
marked@16.3.0: {}
|
||||||
|
|
||||||
match-sorter@6.3.4:
|
match-sorter@6.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
|
|||||||
BIN
public/images/authors/dave-damage/avatar.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
public/images/categories/items/4/background.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/images/covers/meta/index/cover/src.jpeg
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
public/images/figures/index/pexels-clickerhappy-534590.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/images/jon-butterworth-rXjSnBx8Kuc-unsplash.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/images/pexels-chalta-phirta-307182428-32933283.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/images/pexels-clickerhappy-534590.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/images/pexels-david-bartus-43782-450100.jpg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
public/images/pexels-deann-dasilva-1997359-6440968.jpg
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
public/images/pexels-igor-haritanovich-814387-1695050.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/images/pile-of-skulls.jpeg
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
public/images/swamp.jpeg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@@ -9,7 +9,7 @@ export default function SiteLayout({
|
|||||||
<MenuProvider>
|
<MenuProvider>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageMenu />
|
<PageMenu />
|
||||||
<main>{children}</main>
|
{children}
|
||||||
</MenuProvider>
|
</MenuProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/app/(site)/meta/page.tsx
Normal file
@@ -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<Metadata> {
|
||||||
|
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 <Article article={article} tags={tags} author={author} />;
|
||||||
|
}
|
||||||
@@ -6,488 +6,6 @@ export default function Home() {
|
|||||||
<h1>
|
<h1>
|
||||||
DAVE! DAVE! Do Not Let Us Die In The Dark Night Of This Cold Winter!
|
DAVE! DAVE! Do Not Let Us Die In The Dark Night Of This Cold Winter!
|
||||||
</h1>
|
</h1>
|
||||||
<h2>Background / Image effects</h2>
|
|
||||||
<div className={styles.grid}>
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Overexposure Blast</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-overexposure`}
|
|
||||||
src="https://images.pexels.com/photos/1096925/pexels-photo-1096925.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Contrast Slam</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-contrastslam`}
|
|
||||||
src="https://images.pexels.com/photos/119809/pexels-photo-119809.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Colorbleed</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-colorbleed`}
|
|
||||||
src="https://images.pexels.com/photos/12924931/pexels-photo-12924931.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Film Burn</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-filmburn`}
|
|
||||||
src="https://images.pexels.com/photos/1031357/pexels-photo-1031357.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Photocopier Malfunction</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-malfunction`}
|
|
||||||
src="https://images.pexels.com/photos/3054252/pexels-photo-3054252.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Toner Starvation</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-tonerstarvation`}
|
|
||||||
src="https://images.pexels.com/photos/3822728/pexels-photo-3822728.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Digital Corruption</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-corruption`}
|
|
||||||
src="https://images.pexels.com/photos/220793/pexels-photo-220793.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${styles.demoCard}`}>
|
|
||||||
<h3 className={styles.demoTitle}>Stark Flash</h3>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<img
|
|
||||||
className={`${styles.demoImage} anim-starkflash`}
|
|
||||||
src="https://images.pexels.com/photos/13744675/pexels-photo-13744675.jpeg"
|
|
||||||
alt="Demo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.instructions}>
|
|
||||||
<h2>Instructions</h2>
|
|
||||||
<p>
|
|
||||||
Hover over each image to see the effect. These are designed to work
|
|
||||||
with your base filter:
|
|
||||||
</p>
|
|
||||||
<code>filter: grayscale(1) contrast(150%) brightness(120%)</code>
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The effects range from subtle (Contrast Slam) to more dramatic
|
|
||||||
(Digital Corruption). Choose based on how aggressive you want the
|
|
||||||
interaction to feel.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<h2>Link Effects</h2>
|
|
||||||
<div className={styles.grid}>
|
|
||||||
{/* Your Ideas */}
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Strikethrough Mark (Marker)</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-strikethroughmarker">
|
|
||||||
strikethrough link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Strikethrough Mark (Industrial)</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-strikethroughindustrial">
|
|
||||||
strikethrough link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Marker Highlight [Industrial]</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-markerhighlightindustrial">
|
|
||||||
marker highlight link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stamping/Punching Effects */}
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Label Maker</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-labelmaker">
|
|
||||||
label maker link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Rubber Stamp</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-rubberstamp">
|
|
||||||
rubber stamp link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Industrial/Mechanical */}
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Press/Stamp</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-pressstamp">
|
|
||||||
pressed link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Typewriter Underline</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-typewriter">
|
|
||||||
typewriter link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Hard Invert</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-hardInvert">
|
|
||||||
hard invert link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Marking/Annotation */}
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Bracket Annotation</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-pointer">
|
|
||||||
bracket link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Corner Box</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-cornerbox">
|
|
||||||
corner box link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Glitch/Digital */}
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Character Glitch</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-characterglitch">
|
|
||||||
glitch link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Pixel Shift</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-pixelshift">
|
|
||||||
shifting link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Extra Ideas */}
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>Redacted/Censored</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with a{' '}
|
|
||||||
<a href="#" className="anim-redacted">
|
|
||||||
redacted link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.demoCard}>
|
|
||||||
<h3 className={styles.demoTitle}>X-Ray/Negative</h3>
|
|
||||||
<p>
|
|
||||||
This is some text with an{' '}
|
|
||||||
<a href="#" className="anim-xray">
|
|
||||||
x-ray link
|
|
||||||
</a>{' '}
|
|
||||||
in the middle of it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="content">
|
|
||||||
<h1>
|
|
||||||
Successful Isildur's brink again throttle flank tightening splash.
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
Help illusion embrace liquor tightening intelligence Maggot's whips
|
|
||||||
bit forests. 17 sing impassable helps Southrons beheading. What's the
|
|
||||||
Elvish word for 'friend'?
|
|
||||||
</p>
|
|
||||||
<h2>Give Hobbitses lend yours lads picking uniting sometime.</h2>
|
|
||||||
<p>
|
|
||||||
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?
|
|
||||||
</p>
|
|
||||||
<h3>Doorway Mithrandir clearing wielder strengths floor?</h3>
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Mirkwood.</li>
|
|
||||||
<li>Tom.</li>
|
|
||||||
<li>Bilbo's.</li>
|
|
||||||
<li>Gandalf's.</li>
|
|
||||||
<li>Dwarvish.</li>
|
|
||||||
</ul>
|
|
||||||
<ul>
|
|
||||||
<li>Queen wants Oin loose heads decay piety!</li>
|
|
||||||
<li>Large happening arrived owes legends wit war bled Durin's.</li>
|
|
||||||
<li>
|
|
||||||
Pursuit exactly during relief mission meats cause Noldorin ablaze
|
|
||||||
tracked.
|
|
||||||
</li>
|
|
||||||
<li>Darken knife midday meat Goblinses.</li>
|
|
||||||
<li>
|
|
||||||
Your Rabble-rousers greatest could beast thirty-four t wizards
|
|
||||||
slumbers reforge.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Late vagabond knowing Ent legends there flattened cultured?</h3>
|
|
||||||
<p>
|
|
||||||
Swords are no more use here. Deny Chubbs restored. Scare rebuild
|
|
||||||
Argonath tracked day's large.
|
|
||||||
</p>
|
|
||||||
<ol>
|
|
||||||
<li>Easterlings!</li>
|
|
||||||
<li>Bill.</li>
|
|
||||||
<li>Dúnedain!</li>
|
|
||||||
<li>Gandalf.</li>
|
|
||||||
<li>Orcrist.</li>
|
|
||||||
</ol>
|
|
||||||
<ol>
|
|
||||||
<li>Answers feelings Elrond conjurer runs.</li>
|
|
||||||
<li>Sul nudge powerless jelly dumping hair grows log forgave?</li>
|
|
||||||
<li>
|
|
||||||
Retaken succumbed funeral courtyard Glóin incident mere somewhere
|
|
||||||
commander assistance?
|
|
||||||
</li>
|
|
||||||
<li>Bore outrun stead fight Athelas guardroom willing contains.</li>
|
|
||||||
<li>Hunted Angmar wager noose arguing?</li>
|
|
||||||
</ol>
|
|
||||||
<h3>Elros next own wisp whence cakehole right!</h3>
|
|
||||||
<p>
|
|
||||||
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!
|
|
||||||
</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Ales</th>
|
|
||||||
<th>Deeply</th>
|
|
||||||
<th>Mortality</th>
|
|
||||||
<th>Open</th>
|
|
||||||
<th>Single-handedly</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Band</td>
|
|
||||||
<td>appeared</td>
|
|
||||||
<td>waited</td>
|
|
||||||
<td>whose</td>
|
|
||||||
<td>plate</td>
|
|
||||||
<td>marshaling</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Meat</td>
|
|
||||||
<td>nest</td>
|
|
||||||
<td>thatched</td>
|
|
||||||
<td>rallying</td>
|
|
||||||
<td>claimed</td>
|
|
||||||
<td>hungers</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Mouse</td>
|
|
||||||
<td>also</td>
|
|
||||||
<td>birdses</td>
|
|
||||||
<td>moons</td>
|
|
||||||
<td>strain</td>
|
|
||||||
<td>brightest</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Hate</td>
|
|
||||||
<td>different</td>
|
|
||||||
<td>arrangements</td>
|
|
||||||
<td>chiefest</td>
|
|
||||||
<td>think</td>
|
|
||||||
<td>try</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Something's</td>
|
|
||||||
<td>task</td>
|
|
||||||
<td>here's</td>
|
|
||||||
<td>decent</td>
|
|
||||||
<td>someone</td>
|
|
||||||
<td>uses</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Shirt</td>
|
|
||||||
<td>tonight</td>
|
|
||||||
<td>bay</td>
|
|
||||||
<td>beautifully</td>
|
|
||||||
<td>tad</td>
|
|
||||||
<td>cloaks</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td>revenge</td>
|
|
||||||
<td>teaching</td>
|
|
||||||
<td>mischief</td>
|
|
||||||
<td>shores</td>
|
|
||||||
<td>dreams</td>
|
|
||||||
<td>tested</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
<h4>Stage dishcloth 20 horses Tooks souls crawl crime.</h4>
|
|
||||||
<p>
|
|
||||||
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!
|
|
||||||
</p>
|
|
||||||
<blockquote>
|
|
||||||
<p>You are full of surprises, Master Baggins.</p>
|
|
||||||
<footer>
|
|
||||||
—Girion, <cite>warmongering appears</cite>
|
|
||||||
</footer>
|
|
||||||
</blockquote>
|
|
||||||
<h5>
|
|
||||||
Sung outscoring fingers Fundin reaction inquiries buggers deadliest.
|
|
||||||
</h5>
|
|
||||||
<p>
|
|
||||||
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!
|
|
||||||
</p>
|
|
||||||
<pre>
|
|
||||||
Rhudaur fancy tilled heart beggars. Dwarf nothing talked foot club.
|
|
||||||
Slaughtered flatten Hobbit journey's four-day?
|
|
||||||
</pre>
|
|
||||||
<h6>Pearl tact tomb bits Arwen Evenstar worry?</h6>
|
|
||||||
<p>
|
|
||||||
Times unspoiled <time>defenses</time> Silvan.{' '}
|
|
||||||
<del>Sigrid Pippin Gandalf</del> thin stubbornness noises easily
|
|
||||||
spread. Eldar warriors <code>won answered</code> filth yourself
|
|
||||||
pocket. Showing store consistency <kbd>M</kbd> crevice. Decision
|
|
||||||
feverfew <sup>giving</sup> Misty Mountain lord supplant. Gorgoroth{' '}
|
|
||||||
<dfn>load born fulfilled plenty</dfn> fates serpent. Doorstep Pippin's{' '}
|
|
||||||
<cite>pity bridge</cite> long weak weep? Brightest <abbr>Chubbs</abbr>{' '}
|
|
||||||
jewels understand. Somewhat Erebor <sub>noise</sub> squealing moved?
|
|
||||||
Pippin's <mark>feels overrun</mark> hours brown burns. <var>Anor</var>{' '}
|
|
||||||
turning pick prophecy. Surrounded <q>entered needlessly weary</q> vile
|
|
||||||
hmm Bagshot Row. Consent <ins>outwitted dotage slug</ins> Homely hear.
|
|
||||||
Parapet <strong>protected favored defied roam</strong> quiet Dori sick
|
|
||||||
bent. <samp>Homage store hurricane</samp> prove ferret Helm's Deep
|
|
||||||
lately? <small>Excellent regret fun often</small> returned Wood-elves
|
|
||||||
apocalypse. Théodred's rights rat drawing{' '}
|
|
||||||
<a>examine dared bygone residence deeply</a>.{' '}
|
|
||||||
<em>Greenway Girion Rohirrim</em> trammel waiting edge.
|
|
||||||
</p>
|
|
||||||
<hr />
|
|
||||||
<dl>
|
|
||||||
<dt>Turn</dt>
|
|
||||||
<dd>
|
|
||||||
Bare protuberance arrived forging funny salvage Cair except first
|
|
||||||
banners.
|
|
||||||
</dd>
|
|
||||||
<dt>Foes</dt>
|
|
||||||
<dd>Bore river large house shadows it's Tuckborough warn.</dd>
|
|
||||||
<dd>
|
|
||||||
Stirring Greenwood nest sapphire grant gob flagon famous mean!
|
|
||||||
</dd>
|
|
||||||
<dt>Unprepared</dt>
|
|
||||||
<dd>
|
|
||||||
Single-handed wriggling creatures lock canopy anytime horses defense
|
|
||||||
Hobbit's?
|
|
||||||
</dd>
|
|
||||||
<dd>
|
|
||||||
Nûmenor dungeons achieving encourage fretting dines believes
|
|
||||||
understand.
|
|
||||||
</dd>
|
|
||||||
<dd>Ease love shine legs wee harbor Udùn adventure tumble stays.</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/components/Article/Article.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.article {
|
||||||
|
@mixin layout-wrapper;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.wrapper {
|
||||||
|
@mixin mx auto;
|
||||||
|
@mixin px var(--spacing-comfortable);
|
||||||
|
|
||||||
|
max-width: 90ch;
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasMargin {
|
||||||
|
@supports (anchor-name: --test) {
|
||||||
|
@media screen and (--bp-margin) {
|
||||||
|
@mixin px 0;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-generous);
|
||||||
|
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 40ch;
|
||||||
|
max-width: 75ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/components/Article/Content/MarkdocRenderer/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Markdoc from '@markdoc/markdoc';
|
||||||
|
import type { Node } from '@markdoc/markdoc';
|
||||||
|
|
||||||
|
import ContentComponents from '@/components/Content';
|
||||||
|
import SidenoteContainer from '@/components/Content/Sidenote/Container';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface MarkdocRendererProps {
|
||||||
|
content: () => Promise<{ node: Node }>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MARGIN_COMPONENTS = ['Sidenote'];
|
||||||
|
|
||||||
|
export default async function MarkdocRenderer({
|
||||||
|
content,
|
||||||
|
className,
|
||||||
|
}: MarkdocRendererProps) {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={`${styles.wrapper} ${className} ${showMargin ? styles.hasMargin : ''}`}
|
||||||
|
>
|
||||||
|
<div className={`${styles.content} content`}>
|
||||||
|
{Markdoc.renderers.react(renderable, React, {
|
||||||
|
components: ContentComponents,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<SidenoteContainer items={sidenotes} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/components/Article/Content/index.tsx
Normal file
@@ -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 <MarkdocRenderer content={article.content} />;
|
||||||
|
}
|
||||||
0
src/components/Article/Footer/Footer.module.tsx
Normal file
0
src/components/Article/Footer/index.tsx
Normal file
26
src/components/Article/Header/Cover/Cover.module.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.cover {
|
||||||
|
@mixin border-t var(--size-3), solid, var(--color-surface-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 60vh;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
padding: var(--spacing-snug);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
background-color: var(--color-surface-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captionwrapper {
|
||||||
|
font-size: var(--font-size-responsive);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captiontext {
|
||||||
|
font-size: var(--typo-size-sm);
|
||||||
|
}
|
||||||
36
src/components/Article/Header/Cover/index.tsx
Normal file
@@ -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 (
|
||||||
|
<figure className={styles.cover}>
|
||||||
|
<Image
|
||||||
|
src={cover.src}
|
||||||
|
alt={cover.alt || 'Standard Alt'}
|
||||||
|
width={0}
|
||||||
|
height={0}
|
||||||
|
sizes={'100vw'}
|
||||||
|
className={styles.image}
|
||||||
|
/>
|
||||||
|
<figcaption className={styles.caption}>
|
||||||
|
<div className={styles.captionwrapper}>
|
||||||
|
{cover.caption ? (
|
||||||
|
<span className={styles.captiontext}>{cover.caption}</span>
|
||||||
|
) : (
|
||||||
|
<span className={styles.covermeta}>{cover.src}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
src/components/Article/Header/Header.module.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.container {
|
||||||
|
margin-bottom: var(--spacing-comfortable);
|
||||||
|
border-bottom: var(--size-4) solid var(--color-text-primary);
|
||||||
|
}
|
||||||
71
src/components/Article/Header/Meta/Meta.module.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
.meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--spacing-comfortable);
|
||||||
|
font-size: var(--font-size-responsive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
@mixin py var(--spacing-snug);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
@mixin mb var(--spacing-tight);
|
||||||
|
@mixin pb var(--spacing-tight);
|
||||||
|
@mixin border-b var(--size-1), solid, var(--color-surface-inverse);
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
font-size: var(--typo-size-xs);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--typo-spacing-comfortable);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
@mixin anim-txt-characterglitch;
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--typo-size-sm);
|
||||||
|
font-weight: var(--typo-weight-bold);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.taglist {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: var(--spacing-tight) var(--spacing-snug);
|
||||||
|
border: var(--size-1) solid var(--color-palette-charcoal-gray);
|
||||||
|
|
||||||
|
font-family: var(--font-mono), monospace;
|
||||||
|
font-size: var(--typo-size-sm);
|
||||||
|
font-weight: var(--typo-weight-bold);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--typo-spacing-relaxed);
|
||||||
|
|
||||||
|
background: var(--color-palette-charcoal-gray);
|
||||||
|
|
||||||
|
& .link {
|
||||||
|
color: var(--color-palette-off-white);
|
||||||
|
transition: color 0.5s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.updatedate {
|
||||||
|
font-family: var(--font-mono), monospace;
|
||||||
|
font-size: var(--typo-size-sm);
|
||||||
|
font-weight: var(--typo-weight-bold);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
58
src/components/Article/Header/Meta/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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 (
|
||||||
|
<div className={styles.meta}>
|
||||||
|
{author && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<span className={styles.label}>Author</span>
|
||||||
|
<span className={styles.author}>
|
||||||
|
<Link href={author.slug}>{author.name}</Link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<span className={styles.label}>Tags</span>
|
||||||
|
<ul className={styles.taglist}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<li key={tag.slug} className={styles.tag}>
|
||||||
|
<Link className={styles.link} href={`/tags/${tag.slug}`}>
|
||||||
|
{tag.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{publicationDate && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<span className={styles.label}>Last Update</span>
|
||||||
|
<time className={styles.updatedate}>
|
||||||
|
⟫
|
||||||
|
{updateDate
|
||||||
|
? toMilitaryDTG(updateDate)
|
||||||
|
: toMilitaryDTG(publicationDate)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/components/Article/Header/Overline/Overline.module.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.overline {
|
||||||
|
@mixin px var(--spacing-comfortable);
|
||||||
|
|
||||||
|
background-color: var(--color-surface-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
@mixin py var(--spacing-snug);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: var(--typo-size-responsive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--typo-size-sm);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
font-weight: var(--typo-weight-bold);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
transition: color 0.5s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.publicationDate {
|
||||||
|
font-family: var(--font-mono), monospace;
|
||||||
|
font-size: var(--typo-size-xs);
|
||||||
|
font-weight: var(--typo-weight-bold);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
}
|
||||||
48
src/components/Article/Header/Overline/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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 (
|
||||||
|
<li key={index} className={styles.crumb}>
|
||||||
|
<span className={styles.separator}>/</span>
|
||||||
|
{isLast ? (
|
||||||
|
<span className={styles.current}>{crumb}</span>
|
||||||
|
) : (
|
||||||
|
<Link href={href} className={styles.link}>
|
||||||
|
{crumb}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={styles.overline}>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<ul className={styles.breadcrumbs}>
|
||||||
|
<li className={styles.crumb}>
|
||||||
|
<Link href="/" className={styles.link}>
|
||||||
|
dave-dmg.de
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{breadcrumber}
|
||||||
|
</ul>
|
||||||
|
<time className={styles.publicationDate}>
|
||||||
|
⟫ {toMilitaryDTG(publicationDate)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/components/Article/Header/Title/Title.module.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.wrapper {
|
||||||
|
@mixin py var(--spacing-cozy);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: var(--el-h1-font-family), serif;
|
||||||
|
font-size: var(--el-h1-font-size);
|
||||||
|
font-weight: var(--el-h1-font-weight);
|
||||||
|
line-height: var(--el-h1-line-height);
|
||||||
|
color: var(--el-h1-color);
|
||||||
|
text-transform: var(--el-h1-text-transform);
|
||||||
|
letter-spacing: var(--el-h1-letter-spacing);
|
||||||
|
}
|
||||||
13
src/components/Article/Header/Title/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import styles from './Title.module.css';
|
||||||
|
|
||||||
|
interface TitleProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Title({ title }: TitleProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<h1 className={styles.title}>{title}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/Article/Header/index.tsx
Normal file
@@ -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 (
|
||||||
|
<header className={styles.container}>
|
||||||
|
<Overline
|
||||||
|
publicationDate={article.meta.publicationDate}
|
||||||
|
breadcrumbs={breadcrumbs}
|
||||||
|
/>
|
||||||
|
<Title title={article.title} />
|
||||||
|
{cover && isValidCover(cover) && <Cover cover={cover} />}
|
||||||
|
<Meta
|
||||||
|
tags={tags}
|
||||||
|
author={author}
|
||||||
|
updateDate={meta.updateDate}
|
||||||
|
publicationDate={meta.publicationDate}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/components/Article/index.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
.item {
|
||||||
|
& .summary {
|
||||||
|
--el-summary-marker-symbol-closed: "▾";
|
||||||
|
--el-summary-marker-symbol-open: "▾";
|
||||||
|
--el-summary-marker-symbol-transform-open: rotate(180deg);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
padding: var(--spacing-snug);
|
||||||
|
|
||||||
|
font-family: var(--el-summary-font-family);
|
||||||
|
font-size: var(--el-summary-font-size);
|
||||||
|
font-weight: var(--el-summary-font-weight);
|
||||||
|
line-height: var(--el-summary-line-height);
|
||||||
|
color: var(--el-summary-fg);
|
||||||
|
text-transform: var(--el-summary-text-transform);
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
background: var(--el-summary-bg);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: var(--el-summary-marker-symbol-closed);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
overflow: hidden;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
transition: grid-template-rows 2s ease;
|
||||||
|
|
||||||
|
& .inner {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
& .summary {
|
||||||
|
&::before {
|
||||||
|
content: var(--el-summary-marker-symbol-open);
|
||||||
|
transform: var(--el-summary-marker-symbol-transform-open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/components/Content/Accordion/AccordionItem/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './AccordionItem.module.css';
|
||||||
|
|
||||||
|
interface AccordionItemProps {
|
||||||
|
title: string;
|
||||||
|
defaultOpen: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccordionItem({
|
||||||
|
title,
|
||||||
|
defaultOpen,
|
||||||
|
children,
|
||||||
|
}: AccordionItemProps) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(defaultOpen);
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
className={styles.item}
|
||||||
|
open={isOpen}
|
||||||
|
onToggle={(e) => setIsOpen(e.currentTarget.open)}
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
className={styles.summary}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</summary>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.inner}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/components/Content/Accordion/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './Accordion.module.css';
|
||||||
|
|
||||||
|
interface AccordionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Accordion({ children }: AccordionProps) {
|
||||||
|
return <div className={styles.accordion}>{children}</div>;
|
||||||
|
}
|
||||||
27
src/components/Content/Blockquote/Blockquote.module.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.container {
|
||||||
|
margin-block: var(--el-p-vspace-top) var(--el-p-vspace-bottom);
|
||||||
|
padding: var(--spacing-snug) 0 var(--spacing-snug) var(--spacing-comfortable);
|
||||||
|
border-left: var(--size-4) solid var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote {
|
||||||
|
font-family: var(--el-blockquote-font-family), serif;
|
||||||
|
font-size: var(--el-blockquote-font-size);
|
||||||
|
font-weight: var(--el-blockquote-font-weight);
|
||||||
|
font-style: var(--el-blockquote-font-style);
|
||||||
|
line-height: var(--el-blockquote-line-height);
|
||||||
|
color: var(--el-blockquote-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribution,
|
||||||
|
.source {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
@mixin anim-txt-typewriter;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/components/Content/Blockquote/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import styles from './Blockquote.module.css';
|
||||||
|
|
||||||
|
interface BlockquoteProps {
|
||||||
|
quote: string;
|
||||||
|
attribution?: string;
|
||||||
|
source?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Blockquote({
|
||||||
|
quote,
|
||||||
|
attribution,
|
||||||
|
source,
|
||||||
|
url,
|
||||||
|
}: BlockquoteProps) {
|
||||||
|
const hasAttribution = !!attribution;
|
||||||
|
const hasSource = !!source;
|
||||||
|
const hasUrl = !!url;
|
||||||
|
const showFooter = hasSource || hasAttribution;
|
||||||
|
const attributionLinked = hasAttribution && hasUrl && !hasSource;
|
||||||
|
const sourceLinked = hasSource && hasUrl;
|
||||||
|
return (
|
||||||
|
<blockquote className={styles.blockquote}>
|
||||||
|
<div
|
||||||
|
className={styles.quote}
|
||||||
|
dangerouslySetInnerHTML={{ __html: marked.parse(quote) }}
|
||||||
|
/>
|
||||||
|
{showFooter && (
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
{hasAttribution && (
|
||||||
|
<cite className={styles.attribution}>
|
||||||
|
{attributionLinked ? (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
className={styles.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{attribution}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
attribution
|
||||||
|
)}
|
||||||
|
</cite>
|
||||||
|
)}
|
||||||
|
{hasSource && (
|
||||||
|
<cite className={styles.source}>
|
||||||
|
{' '}
|
||||||
|
–
|
||||||
|
{sourceLinked ? (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
className={styles.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{source}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
source
|
||||||
|
)}
|
||||||
|
</cite>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
src/components/Content/Callout/Callout.module.css
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
@layer component {
|
||||||
|
.container {
|
||||||
|
--callout-bg: var(--color-surface-inverse);
|
||||||
|
--callout-fg: var(--color-text-inverse);
|
||||||
|
--callout-symbol: "";
|
||||||
|
--callout-symbol-color: var(--color-text-inverse);
|
||||||
|
|
||||||
|
@mixin my var(--spacing-cozy);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
|
||||||
|
border: var(--size-1) solid var(--color-surface-inverse);
|
||||||
|
box-shadow: 2px 2px 0 oklch(from var(--color-surface-inverse) calc(l - 0.075) c h), 4px 4px 0 oklch(from var(--color-surface-inverse) calc(l - 0.2) c h);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@mixin pt 0.425em;
|
||||||
|
|
||||||
|
content: var(--callout-symbol);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: var(--size-8);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: var(--callout-symbol-color);
|
||||||
|
|
||||||
|
background-color: var(--color-surface-inverse);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@mixin text-xl;
|
||||||
|
|
||||||
|
font-family: var(--font-header);
|
||||||
|
font-weight: var(--typo-weight-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--typo-size-xs);
|
||||||
|
font-weight: var(--typo-weight-black);
|
||||||
|
color: var(--callout-fg);
|
||||||
|
|
||||||
|
background: var(--callout-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
--callout-bg: var(--color-palette-fuchsia);
|
||||||
|
--callout-symbol: "◆";
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
--callout-bg: var(--color-state-info);
|
||||||
|
--callout-symbol: "‽";
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
--callout-bg: var(--color-state-warning);
|
||||||
|
--callout-symbol: "‼";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
--callout-bg: var(--color-palette-lime-green);
|
||||||
|
--callout-symbol: "★";
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler {
|
||||||
|
& .label {
|
||||||
|
@mixin text-xl;
|
||||||
|
|
||||||
|
font-family: var(--font-header);
|
||||||
|
font-weight: var(--typo-weight-black);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "[REDACTED] ";
|
||||||
|
color: var(--color-state-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
min-height: 3em;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████';
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
line-height: inherit;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
letter-spacing: -0.15em;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
background: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .toggle {
|
||||||
|
@util hide-visually;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
& ~ .label {
|
||||||
|
&::before {
|
||||||
|
content: '[REVEALED] ';
|
||||||
|
color: var(--color-state-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .content {
|
||||||
|
&::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/components/Content/Callout/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './Callout.module.css';
|
||||||
|
|
||||||
|
interface CalloutProps {
|
||||||
|
type: 'default' | 'example' | 'info' | 'warning' | 'tip' | 'spoiler';
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Callout({ type, title, children }: CalloutProps) {
|
||||||
|
const isSpoiler = type === 'spoiler';
|
||||||
|
const spoilerID = isSpoiler
|
||||||
|
? `spoiler-${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
: undefined;
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${styles[type]}`}>
|
||||||
|
{isSpoiler ? (
|
||||||
|
<>
|
||||||
|
<input type="checkbox" id={spoilerID} className={styles.toggle} />
|
||||||
|
<label htmlFor={spoilerID} className={styles.label}>
|
||||||
|
{title || 'Show spoiler'}
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className={styles.title}>{title}</p>
|
||||||
|
)}
|
||||||
|
<span className={styles.badge}>{type.toUpperCase()}</span>
|
||||||
|
<div className={`${styles.content}`}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.term {
|
||||||
|
margin-block: var(--el-dt-vspace-top) var(--el-dt-vspace-bottom);
|
||||||
|
padding: var(--el-dt-padding);
|
||||||
|
|
||||||
|
font-family: var(--el-dt-font-family), serif;
|
||||||
|
font-size: var(--el-dt-font-size);
|
||||||
|
font-weight: var(--el-dt-font-weight);
|
||||||
|
line-height: var(--el-dt-line-height);
|
||||||
|
color: var(--el-dt-color);
|
||||||
|
text-transform: var(--el-dt-text-transform);
|
||||||
|
letter-spacing: var(--el-dt-letter-spacing);
|
||||||
|
|
||||||
|
background: var(--el-dt-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.def {
|
||||||
|
margin-block: var(--el-dd-vspace-top) var(--el-dd-vspace-bottom);
|
||||||
|
padding: var(--el-dd-indent);
|
||||||
|
|
||||||
|
font-family: var(--el-dd-font-family), sans-serif;
|
||||||
|
font-size: var(--el-dd-font-size);
|
||||||
|
line-height: var(--el-dd-line-height);
|
||||||
|
color: var(--el-dd-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import styles from './DefinitionItem.module.css';
|
||||||
|
|
||||||
|
interface DefinitionItemProps {
|
||||||
|
term: string;
|
||||||
|
definitions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DefinitionItem({
|
||||||
|
term,
|
||||||
|
definitions,
|
||||||
|
}: DefinitionItemProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<dt className={styles.term}>{term}</dt>
|
||||||
|
{definitions.map((def, idx) => (
|
||||||
|
<dd
|
||||||
|
key={idx}
|
||||||
|
className={styles.def}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked.parseInline(def),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.list {
|
||||||
|
margin-block: var(--el-dl-vspace-top) var(--el-dl-vspace-bottom);
|
||||||
|
font-size: var(--el-dl-font-size);
|
||||||
|
line-height: var(--el-dl-line-height);
|
||||||
|
}
|
||||||
10
src/components/Content/DefinitionList/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './DefinitionList.module.css';
|
||||||
|
|
||||||
|
interface DefinitionListProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
export default function DefinitionList({ children }: DefinitionListProps) {
|
||||||
|
return <dl className={styles.list}>{children}</dl>;
|
||||||
|
}
|
||||||
29
src/components/Content/Figure/Figure.module.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.figure {
|
||||||
|
position: relative;
|
||||||
|
border: var(--size-2) solid var(--color-surface-inverse);
|
||||||
|
clip-path: polygon(0 var(--size-12), var(--size-12) 0, 100% 0, 100% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
padding: var(--spacing-snug);
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--typo-size-sm);
|
||||||
|
line-height: var(--typo-leading-relaxed);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
|
||||||
|
background: var(--color-surface-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
margin-top: var(--spacing-tight);
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--typo-size-xs);
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
}
|
||||||
34
src/components/Content/Figure/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import styles from './Figure.module.css';
|
||||||
|
|
||||||
|
interface FigureProps {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
caption?: string;
|
||||||
|
credit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Figure({ src, alt, caption, credit }: FigureProps) {
|
||||||
|
const hasCaption = !!caption;
|
||||||
|
const hasCredit = !!credit;
|
||||||
|
const showFigcaption = hasCaption || hasCredit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<figure className={styles.figure}>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
className={styles.image}
|
||||||
|
/>
|
||||||
|
{showFigcaption && (
|
||||||
|
<figcaption className={styles.caption}>
|
||||||
|
{hasCaption && <span className={styles.text}>{caption}</span>}
|
||||||
|
{hasCredit && <cite className={styles.credit}>[{credit}]</cite>}
|
||||||
|
</figcaption>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/components/Content/Grid/Column/Column.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.column {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
23
src/components/Content/Grid/Column/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './Column.module.css';
|
||||||
|
|
||||||
|
interface ColumnProps {
|
||||||
|
style: {
|
||||||
|
colspan: number;
|
||||||
|
};
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Column({ style, children }: ColumnProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.column}
|
||||||
|
style={{
|
||||||
|
gridColumn: `span ${style.colspan}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/components/Content/Grid/Row/Row.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
29
src/components/Content/Grid/Row/index.tsx
Normal file
@@ -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 default function Row({ style, children }: RowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.row}`}
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${style.cols}, 1fr)`,
|
||||||
|
gap: `${style.gap}`,
|
||||||
|
alignItems: `${style.align}`,
|
||||||
|
justifyContent: `${style.justify}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.container {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-width: var(--size-96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
@supports (anchor-name: --test) {
|
||||||
|
@media screen and (--bp-margin) {
|
||||||
|
position: absolute;
|
||||||
|
top: anchor(top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
@mixin text-xs;
|
||||||
|
@mixin my var(--spacing-cozy);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
padding-left: var(--size-12);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
|
||||||
|
@supports (anchor-name: --test) {
|
||||||
|
@media screen and (--bp-margin) {
|
||||||
|
min-width: var(--size-64);
|
||||||
|
margin-top: 0;
|
||||||
|
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 {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/components/Content/Sidenote/Container/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
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}` } as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={`${styles.note} ${styles[sidenote.type]}`}>
|
||||||
|
<span className={styles.marker}>
|
||||||
|
<span className={styles.symbol}>{sidenote.marker}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={`${styles.content} content`}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked.parseInline(sidenote.content),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<a href={`#ref-${sidenote.id}`} className={styles.ref}>
|
||||||
|
↩
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/components/Content/Sidenote/Item/Item.module.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.ref {
|
||||||
|
@mixin px var(--spacing-tight);
|
||||||
|
|
||||||
|
scroll-margin-top: calc(var(--el-header-height) + var(--spacing-comfortable));
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
font-weight: var(--typo-weight-black);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
|
||||||
|
transition: color 0.5s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/components/Content/Sidenote/Item/index.tsx
Normal file
@@ -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}` } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
[{marker}]
|
||||||
|
</a>
|
||||||
|
</sup>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/Content/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Row from '@/components/Content/Grid/Row';
|
||||||
|
import Column from '@/components/Content/Grid/Column';
|
||||||
|
import Sidenote from '@/components/Content/Sidenote/Item';
|
||||||
|
import DefinitionList from '@/components/Content/DefinitionList';
|
||||||
|
import DefinitionItem from '@/components/Content/DefinitionList/DefinitionItem';
|
||||||
|
import Accordion from '@/components/Content/Accordion';
|
||||||
|
import AccordionItem from '@/components/Content/Accordion/AccordionItem';
|
||||||
|
import Blockquote from '@/components/Content/Blockquote';
|
||||||
|
import Figure from '@/components/Content/Figure';
|
||||||
|
import Callout from '@/components/Content/Callout';
|
||||||
|
|
||||||
|
const ContentComponents = {
|
||||||
|
Column,
|
||||||
|
Row,
|
||||||
|
Sidenote,
|
||||||
|
DefinitionList,
|
||||||
|
DefinitionItem,
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
Blockquote,
|
||||||
|
Figure,
|
||||||
|
Callout,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentComponents;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
@mixin responsive-wrapper;
|
@mixin layout-wrapper;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -60,7 +60,8 @@
|
|||||||
--area-bg-animation-keyframe: none;
|
--area-bg-animation-keyframe: none;
|
||||||
--area-bg--animation-duration: 0s;
|
--area-bg--animation-duration: 0s;
|
||||||
--area-bg--animation-timing: linear;
|
--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;
|
position: absolute;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
filter: var(--area-bg-filter);
|
filter: var(--area-bg-filter);
|
||||||
|
mix-blend-mode: var(--area-bg-blendmode);
|
||||||
|
|
||||||
@media screen and (--bp-tablet-down) {
|
@media screen and (--bp-tablet-down) {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -93,27 +95,10 @@
|
|||||||
--subtitle-transition: all 0.3s ease-in-out;
|
--subtitle-transition: all 0.3s ease-in-out;
|
||||||
--title-transition: color 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 {
|
&:hover {
|
||||||
--divider-color: var(--color-tertiary);
|
--divider-color: var(--color-tertiary);
|
||||||
--title-color: var(--color-tertiary);
|
--title-color: var(--color-tertiary);
|
||||||
--subtitle-color: var(--color-tertiary);
|
--subtitle-color: var(--color-tertiary);
|
||||||
|
|
||||||
&::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.menu {
|
.menu {
|
||||||
--grid-bg: var(--color-palette-charcoal-gray);
|
--grid-bg: var(--color-palette-charcoal-gray);
|
||||||
--grid-fg: var(--color-palette-light-silver);
|
--grid-fg: var(--color-palette-light-silver);
|
||||||
|
--area-blendmode: normal;
|
||||||
|
|
||||||
/* === MenuTitle Vars === */
|
/* === MenuTitle Vars === */
|
||||||
--title-color: var(--grid-fg);
|
--title-color: var(--grid-fg);
|
||||||
@@ -40,13 +41,14 @@
|
|||||||
--divider-width: var(--size-12);
|
--divider-width: var(--size-12);
|
||||||
--divider-height: var(--size-2);
|
--divider-height: var(--size-2);
|
||||||
--divider-font: var(--font-mono);
|
--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);
|
--divider-padding: 0 var(--typo-spacing-cozy);
|
||||||
--subtitle-font: var(--font-mono);
|
--subtitle-font: var(--font-mono);
|
||||||
--subtitle-color: var(--grid-fg);
|
--subtitle-color: var(--grid-fg);
|
||||||
--subtitle-font-size: var(--typo-size-xl);
|
--subtitle-font-size: var(--typo-size-xl);
|
||||||
--subtitle-text-transform: uppercase;
|
--subtitle-text-transform: uppercase;
|
||||||
--subtitle-letter-spacing: var(--typo-spacing-cozy);
|
--subtitle-letter-spacing: var(--typo-spacing-snug);
|
||||||
--subtitle-transition: none;
|
--subtitle-transition: none;
|
||||||
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
14
src/keystatic/collections/awq/article.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { collection } from '@keystatic/core';
|
||||||
|
|
||||||
|
import { createArticleField } from '@/keystatic/fields/article';
|
||||||
|
|
||||||
|
export default collection({
|
||||||
|
label: 'AWQ - Posts',
|
||||||
|
slugField: 'title',
|
||||||
|
path: 'content/awq/articles/*',
|
||||||
|
format: { contentField: 'content' },
|
||||||
|
entryLayout: 'content',
|
||||||
|
schema: {
|
||||||
|
...createArticleField('awq/articles', 'awq'),
|
||||||
|
},
|
||||||
|
});
|
||||||
7
src/keystatic/collections/awq/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import posts from '@/keystatic/collections/awq/article';
|
||||||
|
|
||||||
|
const awqCollections = {
|
||||||
|
awq_posts: posts,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default awqCollections;
|
||||||
14
src/keystatic/collections/meta/article.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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' },
|
||||||
|
entryLayout: 'content',
|
||||||
|
schema: {
|
||||||
|
...createArticleField('meta'),
|
||||||
|
},
|
||||||
|
});
|
||||||
46
src/keystatic/collections/taxonomy/authors.ts
Normal file
@@ -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}`,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
41
src/keystatic/collections/taxonomy/tags.ts
Normal file
@@ -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 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
32
src/keystatic/components/general/accordion.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { listCollapseIcon } from '@keystar/ui/icon/icons/listCollapseIcon';
|
||||||
|
import { repeating, wrapper } from '@keystatic/core/content-components';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
|
||||||
|
const accordionComponents = {
|
||||||
|
Accordion: repeating({
|
||||||
|
label: 'Accordion',
|
||||||
|
icon: listCollapseIcon,
|
||||||
|
children: ['AccordionItem'],
|
||||||
|
schema: {},
|
||||||
|
}),
|
||||||
|
AccordionItem: wrapper({
|
||||||
|
label: 'Accordion Item',
|
||||||
|
forSpecificLocations: true,
|
||||||
|
schema: {
|
||||||
|
title: fields.text({
|
||||||
|
label: 'Title',
|
||||||
|
validation: {
|
||||||
|
length: {
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defaultOpen: fields.checkbox({
|
||||||
|
label: 'Open by default',
|
||||||
|
defaultValue: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default accordionComponents;
|
||||||
34
src/keystatic/components/general/blockquote.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { textQuoteIcon } from '@keystar/ui/icon/icons/textQuoteIcon';
|
||||||
|
import { block } from '@keystatic/core/content-components';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
|
||||||
|
const blockquoteComponent = {
|
||||||
|
Blockquote: block({
|
||||||
|
label: 'Blockquote',
|
||||||
|
icon: textQuoteIcon,
|
||||||
|
schema: {
|
||||||
|
quote: fields.text({
|
||||||
|
label: 'Quote',
|
||||||
|
multiline: true,
|
||||||
|
validation: {
|
||||||
|
length: {
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
attribution: fields.text({
|
||||||
|
label: 'Attribution',
|
||||||
|
description: 'Author or Speaker',
|
||||||
|
}),
|
||||||
|
source: fields.text({
|
||||||
|
label: 'Source',
|
||||||
|
description: 'Book, article, or document [optional]',
|
||||||
|
}),
|
||||||
|
url: fields.url({
|
||||||
|
label: 'Source URL',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default blockquoteComponent;
|
||||||
47
src/keystatic/components/general/callout.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { wrapper } from '@keystatic/core/content-components';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
import { megaphoneIcon } from '@keystar/ui/icon/icons/megaphoneIcon';
|
||||||
|
|
||||||
|
const calloutComponent = {
|
||||||
|
Callout: wrapper({
|
||||||
|
label: 'Callout',
|
||||||
|
icon: megaphoneIcon,
|
||||||
|
schema: {
|
||||||
|
type: fields.select({
|
||||||
|
label: 'Type',
|
||||||
|
defaultValue: 'default',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Default',
|
||||||
|
value: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Example',
|
||||||
|
value: 'example',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Info',
|
||||||
|
value: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Warning',
|
||||||
|
value: 'warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tip',
|
||||||
|
value: 'tip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Spoiler',
|
||||||
|
value: 'spoiler',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
title: fields.text({
|
||||||
|
label: 'Title',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default calloutComponent;
|
||||||
28
src/keystatic/components/general/definitionlist.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { listTreeIcon } from '@keystar/ui/icon/icons/listTreeIcon';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
import { repeating, block } from '@keystatic/core/content-components';
|
||||||
|
|
||||||
|
const definitionlistComponents = {
|
||||||
|
DefinitionList: repeating({
|
||||||
|
label: 'Definition List',
|
||||||
|
icon: listTreeIcon,
|
||||||
|
children: ['DefinitionItem'],
|
||||||
|
schema: {},
|
||||||
|
}),
|
||||||
|
DefinitionItem: block({
|
||||||
|
label: 'Definition Item',
|
||||||
|
forSpecificLocations: true,
|
||||||
|
schema: {
|
||||||
|
term: fields.text({ label: 'Term' }),
|
||||||
|
definitions: fields.array(
|
||||||
|
fields.text({ label: 'Definition', multiline: true }),
|
||||||
|
{
|
||||||
|
label: 'Definitions',
|
||||||
|
itemLabel: (props) => props.value || 'Definition',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default definitionlistComponents;
|
||||||
35
src/keystatic/components/general/figure.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { imageIcon } from '@keystar/ui/icon/icons/imageIcon';
|
||||||
|
import { block } from '@keystatic/core/content-components';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
|
||||||
|
const figureComponent = {
|
||||||
|
Figure: block({
|
||||||
|
label: 'Figure',
|
||||||
|
icon: imageIcon,
|
||||||
|
schema: {
|
||||||
|
src: fields.image({
|
||||||
|
label: 'Image',
|
||||||
|
directory: 'public/images/figures',
|
||||||
|
publicPath: '/images/figures',
|
||||||
|
}),
|
||||||
|
alt: fields.text({
|
||||||
|
label: 'Alt Text',
|
||||||
|
validation: {
|
||||||
|
length: {
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
caption: fields.text({
|
||||||
|
label: 'Caption',
|
||||||
|
multiline: true,
|
||||||
|
}),
|
||||||
|
credit: fields.text({
|
||||||
|
label: 'Credit/Attribution',
|
||||||
|
description: 'Photographer, artist, or source',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default figureComponent;
|
||||||
62
src/keystatic/components/general/grid.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
import { repeating, wrapper } from '@keystatic/core/content-components';
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
export default gridComponents;
|
||||||
17
src/keystatic/components/general/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import gridComponents from '@/keystatic/components/general/grid';
|
||||||
|
import sidenoteComponents from '@/keystatic/components/general/sidenote';
|
||||||
|
import definitionlistComponents from '@/keystatic/components/general/definitionlist';
|
||||||
|
import accordionComponents from '@/keystatic/components/general/accordion';
|
||||||
|
import blockquoteComponent from '@/keystatic/components/general/blockquote';
|
||||||
|
import figureComponent from '@/keystatic/components/general/figure';
|
||||||
|
import calloutComponent from '@/keystatic/components/general/callout';
|
||||||
|
|
||||||
|
export const generalComponents = {
|
||||||
|
...gridComponents,
|
||||||
|
...sidenoteComponents,
|
||||||
|
...definitionlistComponents,
|
||||||
|
...accordionComponents,
|
||||||
|
...blockquoteComponent,
|
||||||
|
...figureComponent,
|
||||||
|
...calloutComponent,
|
||||||
|
};
|
||||||
61
src/keystatic/components/general/sidenote.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon';
|
||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
import { inline } from '@keystatic/core/content-components';
|
||||||
|
|
||||||
|
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',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sidenoteComponents;
|
||||||
31
src/keystatic/fields/article.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { fields } from '@keystatic/core';
|
||||||
|
|
||||||
|
import { createContentField } from '@/keystatic/fields/content';
|
||||||
|
import { createPathField } from '@/keystatic/fields/general';
|
||||||
|
import { createSEOField } from '@/keystatic/fields/seo';
|
||||||
|
import { createMetaField } from '@/keystatic/fields/meta';
|
||||||
|
|
||||||
|
export const createArticleField = (
|
||||||
|
imageSubfolder: string,
|
||||||
|
defaultPath: string = ''
|
||||||
|
) => ({
|
||||||
|
title: fields.slug({ name: { label: 'Title' } }),
|
||||||
|
summary: fields.text({ label: 'Summary', multiline: true }),
|
||||||
|
path: createPathField(defaultPath),
|
||||||
|
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),
|
||||||
|
});
|
||||||
304
src/keystatic/fields/awq/general.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import { type ComponentSchema, fields } from '@keystatic/core';
|
||||||
|
import { createOperatorField } from '@/keystatic/fields/general';
|
||||||
|
|
||||||
|
export const createCharacteristicReferenceField = (): ComponentSchema =>
|
||||||
|
fields.select({
|
||||||
|
label: 'Characteristic',
|
||||||
|
options: [
|
||||||
|
{ label: 'Weapon Skill', value: 'ws' },
|
||||||
|
{ label: 'Ballistic Skill', value: 'bs' },
|
||||||
|
{ label: 'Strength', value: 's' },
|
||||||
|
{ label: 'Toughness', value: 't' },
|
||||||
|
{ label: 'Initiative', value: 'i' },
|
||||||
|
{ label: 'Dexterity', value: 'dex' },
|
||||||
|
{ label: 'Agility', value: 'ag' },
|
||||||
|
{ label: 'Intelligence', value: 'int' },
|
||||||
|
{ label: 'Willpower', value: 'wp' },
|
||||||
|
{ label: 'Fellowship', value: 'fel' },
|
||||||
|
],
|
||||||
|
defaultValue: 'ws',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSkillReferenceField = (): ComponentSchema =>
|
||||||
|
fields.select({
|
||||||
|
label: 'Skill',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Melee',
|
||||||
|
value: 'melee',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Defence',
|
||||||
|
value: 'defence',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Shooting',
|
||||||
|
value: 'shooting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Throwing',
|
||||||
|
value: 'throwing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Brawn',
|
||||||
|
value: 'brawn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toil',
|
||||||
|
value: 'toil',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Consume Alcohol',
|
||||||
|
value: 'consume-alcohol',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Endurance',
|
||||||
|
value: 'endurance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Perception',
|
||||||
|
value: 'perception',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dodge',
|
||||||
|
value: 'dodge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Skullduggery',
|
||||||
|
value: 'skullduggery',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tradecraft',
|
||||||
|
value: 'tradecraft',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Stealth',
|
||||||
|
value: 'stealth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Athletics',
|
||||||
|
value: 'athletics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Intuition',
|
||||||
|
value: 'intuition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Education',
|
||||||
|
value: 'education',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Discipline',
|
||||||
|
value: 'discipline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Animal Handling',
|
||||||
|
value: 'animal-handling',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Leadership',
|
||||||
|
value: 'leadership',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Charm',
|
||||||
|
value: 'charm',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultValue: 'melee',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSizeReferenceField = (): ComponentSchema =>
|
||||||
|
fields.select({
|
||||||
|
label: 'Size',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Tiny',
|
||||||
|
value: 'tiny',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Little',
|
||||||
|
value: 'little',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Small',
|
||||||
|
value: 'small',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Average',
|
||||||
|
value: 'average',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Large',
|
||||||
|
value: 'large',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Enormous',
|
||||||
|
value: 'enormous',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monstrous',
|
||||||
|
value: 'monstrous',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultValue: 'average',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createPsychologyReferenceField = (): ComponentSchema =>
|
||||||
|
fields.object({
|
||||||
|
type: fields.select({
|
||||||
|
label: 'Psychology Type',
|
||||||
|
defaultValue: 'animosity',
|
||||||
|
options: [
|
||||||
|
{ label: 'Animosity', value: 'animosity' },
|
||||||
|
{ label: 'Fear', value: 'fear' },
|
||||||
|
{ label: 'Frenzy', value: 'frenzy' },
|
||||||
|
{ label: 'Hatred', value: 'hatred' },
|
||||||
|
{ label: 'Prejudice', value: 'prejudice' },
|
||||||
|
{ label: 'Terror', value: 'terror' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
target: fields.text({
|
||||||
|
label: 'Target',
|
||||||
|
description: 'e.g. Elves, Undead, Chaos',
|
||||||
|
}),
|
||||||
|
rating: fields.number({
|
||||||
|
label: 'Rating',
|
||||||
|
description: 'Fear/Terror Only',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRequirementsField = () =>
|
||||||
|
fields.blocks(
|
||||||
|
{
|
||||||
|
characteristic: {
|
||||||
|
label: 'Characteristic',
|
||||||
|
schema: fields.object({
|
||||||
|
characteristic: createCharacteristicReferenceField(),
|
||||||
|
operator: createOperatorField(),
|
||||||
|
value: fields.number({ label: 'Value', defaultValue: 3 }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
skill: {
|
||||||
|
label: 'Skill',
|
||||||
|
schema: fields.object({
|
||||||
|
skill: createSkillReferenceField(),
|
||||||
|
operator: createOperatorField(),
|
||||||
|
value: fields.number({ label: 'Value', defaultValue: 3 }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
label: 'Status',
|
||||||
|
schema: fields.multiselect({
|
||||||
|
label: 'Status Tier',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'copper',
|
||||||
|
label: 'Copper',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'silver',
|
||||||
|
label: 'Silver',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'gold',
|
||||||
|
label: 'gold',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultValue: ['copper'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
talent: {
|
||||||
|
label: 'Talent',
|
||||||
|
schema: fields.relationship({
|
||||||
|
label: 'Talent',
|
||||||
|
collection: 'awq_talents',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
lore: {
|
||||||
|
label: 'Lore',
|
||||||
|
schema: fields.relationship({
|
||||||
|
label: 'Lore',
|
||||||
|
collection: 'awq_lores',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: 'Requirements' }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createTraitsField = (): ComponentSchema =>
|
||||||
|
fields.array(
|
||||||
|
fields.object({
|
||||||
|
name: fields.text({
|
||||||
|
label: 'Trait name',
|
||||||
|
}),
|
||||||
|
description: fields.text({
|
||||||
|
label: 'Description',
|
||||||
|
multiline: true,
|
||||||
|
}),
|
||||||
|
grants: fields.blocks(
|
||||||
|
{
|
||||||
|
talent: {
|
||||||
|
label: 'Talent',
|
||||||
|
schema: fields.relationship({
|
||||||
|
label: 'Talent',
|
||||||
|
collection: 'awq_talents',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
talent_options: {
|
||||||
|
label: 'Talent Options',
|
||||||
|
schema: fields.object({
|
||||||
|
choices: fields.number({
|
||||||
|
label: 'Number to choose',
|
||||||
|
defaultValue: 1,
|
||||||
|
}),
|
||||||
|
options: fields.array(
|
||||||
|
fields.relationship({
|
||||||
|
label: 'Talent Options',
|
||||||
|
collection: 'awq_talents',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
talent_random: {
|
||||||
|
label: 'Random Talent',
|
||||||
|
schema: fields.object({
|
||||||
|
rolls: fields.number({
|
||||||
|
label: 'Rolls',
|
||||||
|
defaultValue: 1,
|
||||||
|
}),
|
||||||
|
table: fields.array(
|
||||||
|
fields.object({
|
||||||
|
roll: fields.object({
|
||||||
|
min: fields.number({ label: 'Min' }),
|
||||||
|
max: fields.number({ label: 'Max' }),
|
||||||
|
}),
|
||||||
|
talent: fields.relationship({
|
||||||
|
label: 'Talent',
|
||||||
|
collection: 'awq_talents',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
psychology: {
|
||||||
|
label: 'Psychology',
|
||||||
|
schema: createPsychologyReferenceField(),
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
label: 'Custom Effect',
|
||||||
|
schema: fields.text({
|
||||||
|
label: 'Custom Effect',
|
||||||
|
multiline: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Trait Grants',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
label: 'Traits',
|
||||||
|
itemLabel: (props) => props.fields.name.value || 'Unnamed Trait',
|
||||||
|
}
|
||||||
|
);
|
||||||
22
src/keystatic/fields/content.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
21
src/keystatic/fields/general.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentSchema, fields } from '@keystatic/core';
|
||||||
|
|
||||||
|
export const createOperatorField = (): ComponentSchema =>
|
||||||
|
fields.select({
|
||||||
|
label: 'Operator',
|
||||||
|
options: [
|
||||||
|
{ label: '>=', value: 'gte' },
|
||||||
|
{ label: '>', value: 'gt' },
|
||||||
|
{ label: '=', value: 'eq' },
|
||||||
|
{ label: '<', value: 'lt' },
|
||||||
|
{ label: '<=', value: 'lte' },
|
||||||
|
],
|
||||||
|
defaultValue: 'eq',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createPathField = (defaultValue: string): ComponentSchema =>
|
||||||
|
fields.text({
|
||||||
|
label: 'Path',
|
||||||
|
description: 'Path on the website',
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
});
|
||||||
43
src/keystatic/fields/meta.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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',
|
||||||
|
}
|
||||||
|
);
|
||||||
24
src/keystatic/fields/seo.ts
Normal file
@@ -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',
|
||||||
|
}
|
||||||
|
);
|
||||||
7
src/lib/markdoc/nodes.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Config } from '@markdoc/markdoc';
|
||||||
|
|
||||||
|
export const nodes: Config['nodes'] = {
|
||||||
|
document: {
|
||||||
|
render: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
127
src/lib/markdoc/tags.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DefinitionList: {
|
||||||
|
render: 'DefinitionList',
|
||||||
|
children: ['tag'],
|
||||||
|
},
|
||||||
|
DefinitionItem: {
|
||||||
|
render: 'DefinitionItem',
|
||||||
|
selfClosing: false,
|
||||||
|
attributes: {
|
||||||
|
term: {
|
||||||
|
type: 'String',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
definitions: {
|
||||||
|
type: 'Array',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Accordion: {
|
||||||
|
render: 'Accordion',
|
||||||
|
children: ['tag'],
|
||||||
|
},
|
||||||
|
AccordionItem: {
|
||||||
|
render: 'AccordionItem',
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'String',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
defaultOpen: {
|
||||||
|
type: 'Boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Blockquote: {
|
||||||
|
render: 'Blockquote',
|
||||||
|
attributes: {
|
||||||
|
quote: {
|
||||||
|
type: 'String',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
attribution: {
|
||||||
|
type: 'String',
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: 'String',
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'String',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Figure: {
|
||||||
|
render: 'Figure',
|
||||||
|
attributes: {
|
||||||
|
src: {
|
||||||
|
type: 'String',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: 'String',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
caption: {
|
||||||
|
type: 'String',
|
||||||
|
},
|
||||||
|
credit: {
|
||||||
|
type: 'String',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Callout: {
|
||||||
|
render: 'Callout',
|
||||||
|
attributes: {
|
||||||
|
type: {
|
||||||
|
type: 'String',
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'String',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
34
src/lib/markdoc/utils.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
14
src/lib/next/generatePageMetaData.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
15
src/lib/readers/meta/posts.ts
Normal file
@@ -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;
|
||||||
|
});
|
||||||
8
src/lib/readers/taxonomy/authors.ts
Normal file
@@ -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;
|
||||||
|
});
|
||||||
8
src/lib/readers/taxonomy/tags.ts
Normal file
@@ -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;
|
||||||
|
});
|
||||||
11
src/lib/types/components.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Sidenote {
|
||||||
|
id: string;
|
||||||
|
marker: string;
|
||||||
|
content: string;
|
||||||
|
type: 'default' | 'lore' | 'crunch' | 'example';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefinitionItem {
|
||||||
|
term: string;
|
||||||
|
definition: string[];
|
||||||
|
}
|
||||||
3
src/lib/types/content.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { MetaContent } from '@/lib/types/meta';
|
||||||
|
|
||||||
|
export type ArticleContent = MetaContent;
|
||||||
8
src/lib/types/meta.ts
Normal file
@@ -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;
|
||||||
8
src/lib/types/taxonomy.ts
Normal file
@@ -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 };
|
||||||
24
src/lib/utils/date.ts
Normal file
@@ -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}`;
|
||||||
|
}
|
||||||
13
src/lib/utils/guards.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||