Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
DrMint | 67de6b0b6a |
11
.env.example
11
.env.example
|
@ -8,19 +8,8 @@ PAYLOAD_USER=myemail@domain.com
|
||||||
PAYLOAD_PASSWORD=somepassword123
|
PAYLOAD_PASSWORD=somepassword123
|
||||||
WEB_HOOK_TOKEN=webhookd5e6ea45ef4e66eaa151612bdcb599df
|
WEB_HOOK_TOKEN=webhookd5e6ea45ef4e66eaa151612bdcb599df
|
||||||
|
|
||||||
## CACHING
|
|
||||||
DATA_CACHING=false
|
|
||||||
DATA_PRECACHING=false
|
|
||||||
PAGE_CACHING=false
|
|
||||||
PAGE_PRECACHING=false
|
|
||||||
CACHE_CONTROL=false
|
|
||||||
|
|
||||||
## OPEN EXCHANGE RATE
|
## OPEN EXCHANGE RATE
|
||||||
OER_APP_ID=oerappid5e6ea45ef4e66eaa151612bdcb599df
|
OER_APP_ID=oerappid5e6ea45ef4e66eaa151612bdcb599df
|
||||||
|
|
||||||
## MEILI
|
|
||||||
MEILISEARCH_URL=https://meilisearch.domain.com
|
|
||||||
MEILISEARCH_MASTER_KEY=some_api_keyqs23d1qs6d54qs897qs3
|
|
||||||
|
|
||||||
## ANALYTICS
|
## ANALYTICS
|
||||||
ANALYTICS_URL=http://analytics.domain.com
|
ANALYTICS_URL=http://analytics.domain.com
|
|
@ -19,6 +19,3 @@ pnpm-debug.log*
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# caching
|
|
||||||
.cache
|
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "src/shared"]
|
|
||||||
path = src/shared
|
|
||||||
url = https://github.com/Accords-Library/shared-library.git
|
|
|
@ -1,2 +1,2 @@
|
||||||
src/shared/*
|
src/shared/*
|
||||||
dist
|
!src/shared/analytics
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"recommendations": ["astro-build.astro-vscode", "antfu.iconify", "esbenp.prettier-vscode"],
|
"recommendations": ["astro-build.astro-vscode", "antfu.iconify"],
|
||||||
"unwantedRecommendations": []
|
"unwantedRecommendations": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
{
|
{
|
||||||
"editor.rulers": [100],
|
"editor.rulers": [100],
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
"explorer.fileNesting.enabled": true,
|
|
||||||
"explorer.fileNesting.patterns": {
|
|
||||||
"package.json": ".git*, package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, .ncurc.*, .nvmrc, *.config.cjs, *.config.js, *.config.ts, *config.json, .*ignore",
|
|
||||||
".env": ".env.*",
|
|
||||||
"README.md": "*.md"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
71
MIGRATION.md
71
MIGRATION.md
|
@ -1,17 +1,17 @@
|
||||||
## DOD
|
## DOD
|
||||||
|
|
||||||
- Jugment chapter 1
|
- Jugment
|
||||||
|
- Scans
|
||||||
|
- Gallery (add the magazine cover)
|
||||||
- Magnitude negative
|
- Magnitude negative
|
||||||
|
- Tags for Chapter 2 and up
|
||||||
|
- Soundtrack
|
||||||
- Missing tags
|
- Missing tags
|
||||||
- Soundtrack vol1
|
- Missing scans booklet / images
|
||||||
- Missing scans
|
- Missing vol2 cd
|
||||||
- booklet?
|
|
||||||
- Soundtrack vol2
|
|
||||||
- Missing cd
|
|
||||||
- Missing scans
|
|
||||||
- booklet?
|
|
||||||
- https://v3.accords-library.com/en/pages/song-of-fourteen-years
|
- https://v3.accords-library.com/en/pages/song-of-fourteen-years
|
||||||
- missing source to https://web.archive.org/web/20151104092843/http://kho-dazat.tumblr.com/post/73835598260/you-are-the-light-the-light-that-illuminates-my
|
- missing source to https://web.archive.org/web/20151104092843/http://kho-dazat.tumblr.com/post/73835598260/you-are-the-light-the-light-that-illuminates-my
|
||||||
|
- missing parent collectible
|
||||||
- Character novellas, The content originate -> The content originates
|
- Character novellas, The content originate -> The content originates
|
||||||
- https://v3.accords-library.com/en/folders/drakengard-magnitude-negative add previous/follow-up links to chapters
|
- https://v3.accords-library.com/en/folders/drakengard-magnitude-negative add previous/follow-up links to chapters
|
||||||
|
|
||||||
|
@ -19,19 +19,33 @@
|
||||||
|
|
||||||
- https://v3.accords-library.com/en/folders/drakengard-promotional-videos
|
- https://v3.accords-library.com/en/folders/drakengard-promotional-videos
|
||||||
- https://v3.accords-library.com/en/videos/663354cb12807e2ec1b33924
|
- https://v3.accords-library.com/en/videos/663354cb12807e2ec1b33924
|
||||||
|
- https://v3.accords-library.com/en/collectibles/dod-scenario-script
|
||||||
|
- https://v3.accords-library.com/en/collectibles/dod-window-script
|
||||||
|
- https://v3.accords-library.com/en/pages/dod1-questions-and-answers
|
||||||
|
- https://v3.accords-library.com/en/pages/looking-back-1
|
||||||
|
- https://v3.accords-library.com/en/pages/song-of-fourteen-years
|
||||||
|
|
||||||
- Missing collectibles
|
- Missing collectibles
|
||||||
|
|
||||||
|
- https://accords-library.com/library/dod-the-archive-of
|
||||||
|
- https://accords-library.com/library/dod-reservation-promo-dvd
|
||||||
|
- https://accords-library.com/library/dod-promo-dvd
|
||||||
- Games
|
- Games
|
||||||
- https://accords-library.com/library/dod-java
|
- https://accords-library.com/library/dod-java
|
||||||
- https://accords-library.com/library/dod-demo
|
- https://accords-library.com/library/dod-demo
|
||||||
|
|
||||||
- Missing pages
|
- Missing pages
|
||||||
|
- https://accords-library.com/contents/super-space-time-character-exchange-roundtable-in-shinjuku
|
||||||
- https://accords-library.com/contents/dod-history
|
- https://accords-library.com/contents/dod-history
|
||||||
|
|
||||||
## DOD1.3
|
## DOD1.3
|
||||||
|
|
||||||
- Drag-on Dragoon - Fatal Crimson
|
- Drag-on Dragoon - Shi ni Itaru Aka
|
||||||
|
- Basic Knowledge of Drakengard
|
||||||
|
|
||||||
|
- Missing parent collectible
|
||||||
|
|
||||||
|
- https://v3.accords-library.com/en/folders/drakengard-1-3
|
||||||
|
|
||||||
- Missing footnotes
|
- Missing footnotes
|
||||||
- https://v3.accords-library.com/en/pages/dod1-3-a-2040-battle-with-the-empire
|
- https://v3.accords-library.com/en/pages/dod1-3-a-2040-battle-with-the-empire
|
||||||
|
@ -41,19 +55,23 @@
|
||||||
## DOD 2
|
## DOD 2
|
||||||
|
|
||||||
- Missing pages
|
- Missing pages
|
||||||
|
|
||||||
- https://accords-library.com/contents/folder/dod2-promotional-materials
|
- https://accords-library.com/contents/folder/dod2-promotional-materials
|
||||||
|
- https://accords-library.com/contents/super-space-time-character-exchange-roundtable-in-shinjuku
|
||||||
- https://accords-library.com/contents/dod-history
|
- https://accords-library.com/contents/dod-history
|
||||||
|
|
||||||
- Missing collectibles
|
- Missing collectibles
|
||||||
|
|
||||||
|
- Games
|
||||||
|
- https://accords-library.com/library/dod2-demo
|
||||||
- Soundtrack
|
- Soundtrack
|
||||||
- https://accords-library.com/library/hitori-mika-nakashima
|
- https://accords-library.com/library/hitori-mika-nakashima
|
||||||
- https://accords-library.com/library/sirius-eir-aoi
|
- https://accords-library.com/library/sirius-eir-aoi
|
||||||
|
- https://accords-library.com/library/dod2-original-soundtrack
|
||||||
- Missing content
|
- https://accords-library.com/library/dod2-scenario-script-a
|
||||||
|
- https://accords-library.com/library/dod2-scenario-script-b
|
||||||
- https://v3.accords-library.com/en/collectibles/dod2-web-material-disc
|
- https://accords-library.com/library/dod2-story-side
|
||||||
|
- https://accords-library.com/library/dod2-the-archive-of
|
||||||
|
- https://accords-library.com/library/dod2-web-material
|
||||||
|
- https://accords-library.com/library/dod2-strategy-guidebook
|
||||||
|
|
||||||
- Missing footnotes
|
- Missing footnotes
|
||||||
|
|
||||||
|
@ -65,7 +83,6 @@
|
||||||
## DOD3
|
## DOD3
|
||||||
|
|
||||||
- Missing collectibles
|
- Missing collectibles
|
||||||
|
|
||||||
- https://accords-library.com/library/utahime-five
|
- https://accords-library.com/library/utahime-five
|
||||||
- https://accords-library.com/library/dod3-official-score-book
|
- https://accords-library.com/library/dod3-official-score-book
|
||||||
- Soundtrack
|
- Soundtrack
|
||||||
|
@ -73,7 +90,6 @@
|
||||||
- https://accords-library.com/library/dod3-original-soundtrack
|
- https://accords-library.com/library/dod3-original-soundtrack
|
||||||
|
|
||||||
- Missing content
|
- Missing content
|
||||||
|
|
||||||
- https://v3.accords-library.com/en/collectibles/dod-chips-music
|
- https://v3.accords-library.com/en/collectibles/dod-chips-music
|
||||||
- https://v3.accords-library.com/en/collectibles/dod-history-films
|
- https://v3.accords-library.com/en/collectibles/dod-history-films
|
||||||
- https://accords-library.com/library/dod3-collector-edition
|
- https://accords-library.com/library/dod3-collector-edition
|
||||||
|
@ -82,30 +98,19 @@
|
||||||
|
|
||||||
- https://accords-library.com/contents/prophecy
|
- https://accords-library.com/contents/prophecy
|
||||||
- https://accords-library.com/contents/dod3-complete-guide-epilogue
|
- https://accords-library.com/contents/dod3-complete-guide-epilogue
|
||||||
|
- https://accords-library.com/contents/super-space-time-character-exchange-roundtable-in-shinjuku
|
||||||
- https://accords-library.com/contents/dod-history
|
- https://accords-library.com/contents/dod-history
|
||||||
|
|
||||||
- Missing parent page
|
- Missing parent page
|
||||||
|
|
||||||
- Novellas
|
- Novellas
|
||||||
- https://accords-library.com/contents/folder/dod3-dlc-short-stories
|
- https://accords-library.com/contents/folder/dod3-prelude-novellas
|
||||||
|
- https://accords-library.com/contents/folder/dod3-dlc-short-stories
|
||||||
|
- Story side
|
||||||
|
- https://accords-library.com/contents/folder/dod3-story-side
|
||||||
|
|
||||||
- Missing scans
|
- Missing scans
|
||||||
|
|
||||||
- https://v3.accords-library.com/en/collectibles/novel-prelude
|
- https://v3.accords-library.com/en/collectibles/novel-prelude
|
||||||
- Wrong rotation on pages for https://v3.accords-library.com/en/collectibles/visual-artbook/scans
|
- Wrong rotation on pages for https://v3.accords-library.com/en/collectibles/visual-artbook/scans
|
||||||
- https://v3.accords-library.com/en/collectibles/dod10-dlc-code
|
- https://v3.accords-library.com/en/collectibles/dod10-dlc-code
|
||||||
|
|
||||||
- Check release data for https://v3.accords-library.com/en/collectibles/dod10th-anniversary-box and its subitems
|
- Check release data for https://v3.accords-library.com/en/collectibles/dod10th-anniversary-box and its subitems
|
||||||
|
|
||||||
- Missing footnotes
|
|
||||||
- https://v3.accords-library.com/en/pages/super-space-time-character-exchange-roundtable-in-shinjuku
|
|
||||||
- *Like mother, like daughter. I think that's just it… Why I must protect Yonah for the rest of my life! So we can always, always be together… *The proverb here in Japanese directly translates to “You can’t fight blood.”, a likely prod at Furiae’s lust for Caim.
|
|
||||||
|
|
||||||
# NieR
|
|
||||||
|
|
||||||
- Novellas
|
|
||||||
|
|
||||||
- Add credits to wikia or SolemCube
|
|
||||||
- Add more tags
|
|
||||||
|
|
||||||
- https://v3.accords-library.com/en/collectibles/grimoire-nier-revised-edition
|
|
||||||
|
|
45
README.md
45
README.md
|
@ -1,7 +1,5 @@
|
||||||
# Accord's Library
|
# Accord's Library
|
||||||
|
|
||||||
Accord's Library v3.0 (shorten to AL3.0)
|
|
||||||
|
|
||||||
## Tech overview
|
## Tech overview
|
||||||
|
|
||||||
- Client-side framework: None
|
- Client-side framework: None
|
||||||
|
@ -9,20 +7,18 @@ Accord's Library v3.0 (shorten to AL3.0)
|
||||||
- Content management system: [Payload](https://payloadcms.com)
|
- Content management system: [Payload](https://payloadcms.com)
|
||||||
- Database: MongoDB
|
- Database: MongoDB
|
||||||
|
|
||||||
## Core principles
|
## Core
|
||||||
|
|
||||||
|
- https://resilientwebdesign.com/
|
||||||
|
|
||||||
|
Accord's Library v3.0 (shorten to AL3.0) follows the Metamodernist Web model described by Frédéric Bonnet in his article [From Classicism to Metamodernism — A Short History of Web Development](https://dev.to/fredericbonnet/the-third-age-of-web-development-kgj#the-metamodernist-period).
|
||||||
|
|
||||||
- Embrace web standards instead of reinventing the wheel
|
- Embrace web standards instead of reinventing the wheel
|
||||||
- [Progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement). Ensure all users can access and use AL3.0's core functionalities.
|
- [Progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement): SSG or SSR for noscript clients. SPA-like enhancements such as partial page updates and view transitions for clients with JS support.
|
||||||
- End-user preferences are respected.
|
|
||||||
- Mimimal dependencies. Dependencies can be self-hosted or loaded directly from CDNs instead of being bundled up.
|
- Mimimal dependencies. Dependencies can be self-hosted or loaded directly from CDNs instead of being bundled up.
|
||||||
- Accessible, fast, lightweight, substainable
|
- Accessible, fast, lightweight, substainable
|
||||||
- Complexity is moved away from client devices
|
- Complexity is moved away from client devices
|
||||||
|
|
||||||
Read more:
|
|
||||||
|
|
||||||
- [Resilient web design](https://resilientwebdesign.com)
|
|
||||||
- [From Classicism to Metamodernism — A Short History of Web Development](https://dev.to/fredericbonnet/the-third-age-of-web-development-kgj#the-metamodernist-period)
|
|
||||||
|
|
||||||
## Focal points
|
## Focal points
|
||||||
|
|
||||||
- Progressive enhancement / Graceful degradation
|
- Progressive enhancement / Graceful degradation
|
||||||
|
@ -72,28 +68,18 @@ Read more:
|
||||||
- Lazy loaded
|
- Lazy loaded
|
||||||
- Space reservation to reduce Cumulative Layout Shift
|
- Space reservation to reduce Cumulative Layout Shift
|
||||||
- Use of efficient formats (mostly WebP) and meaningful quality settings
|
- Use of efficient formats (mostly WebP) and meaningful quality settings
|
||||||
- Server side rendered
|
- Server side rendered (both good and bad for speed)
|
||||||
- Reduced data transfer
|
- Reduced data transfer
|
||||||
- Reduced client-side complexity
|
- Reduced client-side complexity
|
||||||
- Advanced caching
|
- Would require edge computing to reduce latency
|
||||||
- Data caching
|
- Astro built-in View transitions and client-side navigation
|
||||||
- Reponses from the CMS are cached to speed up response time.
|
- Some data caching between the web server and CMS (to be improved)
|
||||||
- When there is a change on the CMS, the cache is alerted through webhooks.
|
|
||||||
- The impacted responses are then invalidated, fetched again, and cached
|
|
||||||
- All possible reponses from the CMS are precached when the server loads.
|
|
||||||
- The cache is also saved on disk after any change, and is loading at startup if available.
|
|
||||||
- Page caching
|
|
||||||
- The pages themselves are cached to further speed up response time.
|
|
||||||
- When responses in the Data caching are invalidated, cached pages that depend on those reponses are also invalidated. They are then regenerated and cached again.
|
|
||||||
- Similarly to the data cache, all pages are precached when the server loads and the cache is saved on disk and loaded at startup.
|
|
||||||
- The server use the `Last-Modified` header and serve a `304 Not Modified` response when appropriate.
|
|
||||||
- The pages themselves use the `Cache-Control` header to allow local caching on the visitors' web browsers.
|
|
||||||
|
|
||||||
- SEO
|
- SEO
|
||||||
|
|
||||||
- Good defaults for the metadata and OpenGraph properties
|
- Good defaults for the metadata and OpenGraph properties
|
||||||
- Each page can provide a custom title, description, thumbnail, video, audio to be used
|
- Each page can provide a custom title, description, thumbnail, video, audio to be used
|
||||||
- Each language variants are indexed seperately.
|
- Each language variants are indexes seperately.
|
||||||
|
|
||||||
- Complexity
|
- Complexity
|
||||||
- The complexity should be moved away from public-facing parts of the codebase
|
- The complexity should be moved away from public-facing parts of the codebase
|
||||||
|
@ -106,6 +92,7 @@ Read more:
|
||||||
- Handle user interactions
|
- Handle user interactions
|
||||||
- On the client device, there should be minimal complexity
|
- On the client device, there should be minimal complexity
|
||||||
- Handle responsiveness
|
- Handle responsiveness
|
||||||
|
- Handle view transitions (if JS is available)
|
||||||
- Use of web standards: let the browser handle most of the client-side complexity
|
- Use of web standards: let the browser handle most of the client-side complexity
|
||||||
|
|
||||||
## Enhancement provided with JavaScript
|
## Enhancement provided with JavaScript
|
||||||
|
@ -118,7 +105,6 @@ Read more:
|
||||||
|
|
||||||
- Smooth scrolling when using anchor links
|
- Smooth scrolling when using anchor links
|
||||||
- On image pages (scans, gallery, image files), allow the user to navigate to the previous or next image using keyboard arrows.
|
- On image pages (scans, gallery, image files), allow the user to navigate to the previous or next image using keyboard arrows.
|
||||||
- On the search page, allow the user to navigate to the previous or next page using keyboard arrows.
|
|
||||||
|
|
||||||
- On media pages (scans, images, audios, videos), provide a download button. This way, the user doesn't have to right-click -> "save media as..."
|
- On media pages (scans, images, audios, videos), provide a download button. This way, the user doesn't have to right-click -> "save media as..."
|
||||||
|
|
||||||
|
@ -142,6 +128,13 @@ A dotted texture is displayed on the page background. It uses `background-blend-
|
||||||
- Replacing the image with a non-transparent image where the blending is baked-in
|
- Replacing the image with a non-transparent image where the blending is baked-in
|
||||||
- Check if there are ways to make the blending work on iOS
|
- Check if there are ways to make the blending work on iOS
|
||||||
|
|
||||||
|
### Parallax effect
|
||||||
|
|
||||||
|
A parallax effect is applied on the webpages' background image. This effect can be a bit demanding, it is disabled on mobiles and tablets to lessen the impact. Other alternatives could include:
|
||||||
|
|
||||||
|
- Removing the effet entirely
|
||||||
|
- Moving away from JavaScript and using CSS parallax tricks (transform 3D, sticky)
|
||||||
|
|
||||||
## CSS Utility classes
|
## CSS Utility classes
|
||||||
|
|
||||||
- `.dark-theme`: force dark theming to the element and its children.
|
- `.dark-theme`: force dark theming to the element and its children.
|
||||||
|
|
20
TODO.md
20
TODO.md
|
@ -2,23 +2,18 @@
|
||||||
|
|
||||||
## Ongoing
|
## Ongoing
|
||||||
|
|
||||||
- [Bugs] It's possible to save a page with empty Line or Cue
|
|
||||||
- [Bugs] Keziah reported some lag spikes when scrolling on the home page (Firefox on Windows)
|
- [Bugs] Keziah reported some lag spikes when scrolling on the home page (Firefox on Windows)
|
||||||
- [Feat] [Analytics] Add analytics
|
- [Feat] [Analytics] Add analytics
|
||||||
- [Bugs] [Tooltips] Tooltip in under next element (example in timeline)
|
- [Bugs] [Tooltips] Tooltip in under next element (example in timeline)
|
||||||
- [Bugs] [KeyboardNav]:
|
- [Bugs] [Language override] Maso actor is not focusable with keyboard nav
|
||||||
- Maso actor is not focusable with keyboard nav
|
|
||||||
- Parent pages not focusable
|
|
||||||
- Search button is double-focusable (once the link, and one the button)
|
|
||||||
|
|
||||||
## Short term
|
## Short term
|
||||||
|
|
||||||
- [Feat] Add links to all the timeline image and document on Timeline page
|
- [Feat] 404, 500 pages
|
||||||
- [Bugs] Make sure uploads name are slug-like and with an extension.
|
|
||||||
- [Bugs] Nyupun can't upload subtitles files
|
|
||||||
- [Bugs] https://v3.accords-library.com/en/collectibles/dod-original-soundtrack/scans obi is way too big
|
|
||||||
- [Feat] [RichTextContent] Handle relationship
|
- [Feat] [RichTextContent] Handle relationship
|
||||||
- [Feat] [Timeline] Improve layout/spacing on mobile
|
- [Feat] [Timeline] Improve layout/spacing on mobile
|
||||||
|
- [Feat] Display if a content has a source language
|
||||||
|
- [Feat] [JSLess] Display if a content is available in more than one language
|
||||||
- [Bugs] Number of audio players seems limited (on Chrome and Firefox)
|
- [Bugs] Number of audio players seems limited (on Chrome and Firefox)
|
||||||
- [Feat] [RichTextContent] Add autolink block support
|
- [Feat] [RichTextContent] Add autolink block support
|
||||||
|
|
||||||
|
@ -26,7 +21,7 @@
|
||||||
|
|
||||||
- [Feat] Improve page load speed by using
|
- [Feat] Improve page load speed by using
|
||||||
- streaming https://docs.astro.build/en/recipes/streaming-improve-page-performance/
|
- streaming https://docs.astro.build/en/recipes/streaming-improve-page-performance/
|
||||||
- https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
|
- ISR https://logsnag.com/blog/implementing-isr-in-astro
|
||||||
- [Feat] History replace instead of push when browsing scans and gallery
|
- [Feat] History replace instead of push when browsing scans and gallery
|
||||||
- [Feat] Use subgrid to align the generic previews
|
- [Feat] Use subgrid to align the generic previews
|
||||||
- [Bugs] [Timeline] Error if collectible not published?
|
- [Bugs] [Timeline] Error if collectible not published?
|
||||||
|
@ -40,8 +35,9 @@
|
||||||
|
|
||||||
## Long term
|
## Long term
|
||||||
|
|
||||||
- [Feat] Invalidate Back/Forward Cache when changing language/theme/currency
|
- [Feat] Speed up loading with https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
|
||||||
- [Feat] Hovering on a preview card could give a more detailed summary/preview (with all attributes)
|
- [Feat] More data caching between the CMS and Astro
|
||||||
|
- Etag? Last-Modified? Cache-control
|
||||||
- [Feat] Explore posibilities for View Transitions
|
- [Feat] Explore posibilities for View Transitions
|
||||||
- [Feat] Revemp theme system using light-dark https://caniuse.com/mdn-css_types_color_light-dark
|
- [Feat] Revemp theme system using light-dark https://caniuse.com/mdn-css_types_color_light-dark
|
||||||
- [Feat] Add reduce motion to element that zoom when hovering
|
- [Feat] Add reduce motion to element that zoom when hovering
|
||||||
|
|
|
@ -11,22 +11,10 @@ export default defineConfig({
|
||||||
srcDir: "./src",
|
srcDir: "./src",
|
||||||
publicDir: "./public",
|
publicDir: "./public",
|
||||||
outDir: "./dist",
|
outDir: "./dist",
|
||||||
trailingSlash: "never",
|
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: "standalone",
|
mode: "standalone",
|
||||||
}),
|
}),
|
||||||
integrations: [
|
integrations: [
|
||||||
// We can't just call a function on startup because of some Vite's BS...
|
|
||||||
// So instead I'm exposing some endpoint that only does something the first time it's called.
|
|
||||||
{
|
|
||||||
name: "on-server-start",
|
|
||||||
hooks: {
|
|
||||||
"astro:config:done": () => {
|
|
||||||
console.log("Running on startup script in 10s...");
|
|
||||||
setTimeout(() => fetch(`http://${ASTRO_HOST}:${ASTRO_PORT}/en/api/on-startup`), 10_000);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
astroMetaTags(),
|
astroMetaTags(),
|
||||||
icon({
|
icon({
|
||||||
include: {
|
include: {
|
||||||
|
@ -35,7 +23,7 @@ export default defineConfig({
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
prefetch: false,
|
prefetch: false,
|
||||||
devToolbar: { enabled: false },
|
// devToolbar: { enabled: false },
|
||||||
server: {
|
server: {
|
||||||
port: parseInt(ASTRO_PORT ?? "4321"),
|
port: parseInt(ASTRO_PORT ?? "4321"),
|
||||||
host: ASTRO_HOST ?? true,
|
host: ASTRO_HOST ?? true,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "v3.accords-library.com",
|
"name": "v3.accords-library.com",
|
||||||
"version": "3.0.0-beta.9",
|
"version": "3.0.0-beta.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
|
@ -8,33 +8,34 @@
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"upgrade": "ncu",
|
"upgrade": "ncu",
|
||||||
"fetch-submodules": "cd src/shared && git pull && cd ../..",
|
"script:download-payload-sdk": "npm run scripts/download-payload-sdk.ts",
|
||||||
"script:download-currencies": "node --env-file=.env scripts/download-currencies.mjs",
|
"script:download-currencies": "npm run scripts/download-currencies.ts",
|
||||||
"script:download-wording-keys": "npm run scripts/download-wording-keys.ts",
|
"script:download-wording-keys": "npm run scripts/download-wording-keys.ts",
|
||||||
"prettier": "prettier --write --list-different --plugin=prettier-plugin-astro .",
|
"prettier": "prettier --write --list-different --plugin=prettier-plugin-astro .",
|
||||||
"precommit": "npm run fetch-submodules && npm run script:download-currencies && npm run script:download-wording-keys && npm run prettier && npm run astro check"
|
"precommit": "npm run script:download-currencies && npm run script:download-payload-sdk && npm run script:download-wording-keys && npm run prettier && npm run astro check"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=10.0.0",
|
"npm": ">=10.0.0",
|
||||||
"node": ">=19.7.0"
|
"node": ">=19.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.2",
|
"@astrojs/check": "^0.7.0",
|
||||||
"@astrojs/node": "^8.3.3",
|
"@astrojs/node": "^8.3.0",
|
||||||
"accept-language": "^3.0.20",
|
"accept-language": "^3.0.18",
|
||||||
"astro": "4.14.0",
|
"astro": "4.10.2",
|
||||||
"astro-icon": "^1.1.0",
|
"astro-icon": "^1.1.0",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"ua-parser-js": "^1.0.38"
|
"ua-parser-js": "^1.0.38"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/material-symbols": "^1.1.87",
|
"@iconify-json/material-symbols": "^1.1.82",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"astro-meta-tags": "^0.3.0",
|
"astro-meta-tags": "^0.3.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss-preset-env": "^10.0.1",
|
"postcss-preset-env": "^9.5.14",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 400 KiB |
Binary file not shown.
Before Width: | Height: | Size: 129 KiB |
Binary file not shown.
Before Width: | Height: | Size: 39 KiB |
|
@ -1,4 +1,2 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /
|
Disallow: /
|
||||||
Disallow: /*/api/
|
|
||||||
Disallow: /*/dev/
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
rm -r dist
|
|
||||||
npm ci
|
npm ci
|
||||||
npm run precommit
|
npm run precommit
|
||||||
npm run build
|
npm run build
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @ts-check
|
import { writeFileSync, readFileSync, existsSync } from "fs";
|
||||||
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
||||||
|
|
||||||
const OPEN_EXCHANGE_FOLDER = `${process.cwd()}/src/dist/openExchange`;
|
const OPEN_EXCHANGE_FOLDER = `${process.cwd()}/src/shared/openExchange`;
|
||||||
const RATE_JSON_PATH = `${OPEN_EXCHANGE_FOLDER}/rates.json`;
|
const RATE_JSON_PATH = `${OPEN_EXCHANGE_FOLDER}/rates.json`;
|
||||||
const CURRENCIES_JSON_PATH = `${OPEN_EXCHANGE_FOLDER}/currencies.json`;
|
const CURRENCIES_JSON_PATH = `${OPEN_EXCHANGE_FOLDER}/currencies.json`;
|
||||||
const ONE_DAY_IN_MS = 1_000 * 60 * 60 * 24;
|
const ONE_DAY_IN_MS = 1_000 * 60 * 60 * 24;
|
||||||
|
@ -18,21 +17,15 @@ if (existsSync(RATE_JSON_PATH)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.OER_APP_ID) {
|
const ratesUrl = `https://openexchangerates.org/api/latest.json?app_id=${
|
||||||
throw new Error("Missing OER_APP_ID env variable");
|
import.meta.env.OER_APP_ID
|
||||||
}
|
}`;
|
||||||
|
|
||||||
const ratesUrl = `https://openexchangerates.org/api/latest.json?app_id=${process.env.OER_APP_ID}`;
|
|
||||||
const currenciesUrl = `https://openexchangerates.org/api/currencies.json?app_id=${
|
const currenciesUrl = `https://openexchangerates.org/api/currencies.json?app_id=${
|
||||||
process.env.OER_APP_ID
|
import.meta.env.OER_APP_ID
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const rates = await fetch(ratesUrl);
|
const rates = await fetch(ratesUrl);
|
||||||
|
|
||||||
if (!existsSync(OPEN_EXCHANGE_FOLDER)) {
|
|
||||||
mkdirSync(OPEN_EXCHANGE_FOLDER, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rates.ok) {
|
if (rates.ok) {
|
||||||
writeFileSync(RATE_JSON_PATH, await rates.text(), {
|
writeFileSync(RATE_JSON_PATH, await rates.text(), {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { writeFileSync } from "fs";
|
||||||
|
|
||||||
|
const PAYLOAD_FOLDER = `${process.cwd()}/src/shared/payload`;
|
||||||
|
|
||||||
|
const sdk = await fetch(`${import.meta.env.PAYLOAD_API_URL}/sdk`);
|
||||||
|
|
||||||
|
if (!sdk.ok) {
|
||||||
|
console.error("Failed to get the sdk", sdk.status, sdk.statusText);
|
||||||
|
} else {
|
||||||
|
const sdkFile = await sdk.text();
|
||||||
|
writeFileSync(`${PAYLOAD_FOLDER}/payload-sdk.ts`, sdkFile, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,16 +1,10 @@
|
||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from "fs";
|
||||||
import { PayloadSDK } from "src/shared/payload/sdk";
|
import { payload } from "src/utils/payload";
|
||||||
|
|
||||||
const TRANSLATION_FOLDER = `${process.cwd()}/src/i18n`;
|
const TRANSLATION_FOLDER = `${process.cwd()}/src/i18n`;
|
||||||
|
|
||||||
const payload = new PayloadSDK(
|
|
||||||
import.meta.env.PAYLOAD_API_URL,
|
|
||||||
import.meta.env.PAYLOAD_USER,
|
|
||||||
import.meta.env.PAYLOAD_PASSWORD
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: wordings } = await payload.getWordings();
|
const wordings = await payload.getWordings();
|
||||||
const keys = wordings.map(({ name }) => name);
|
const keys = wordings.map(({ name }) => name);
|
||||||
|
|
||||||
let result = "";
|
let result = "";
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
import type {
|
|
||||||
EndpointLanguage,
|
|
||||||
EndpointWebsiteConfig,
|
|
||||||
EndpointWording,
|
|
||||||
} from "src/shared/payload/endpoint-types";
|
|
||||||
import { SDKEndpointNames, type PayloadSDK } from "src/shared/payload/sdk";
|
|
||||||
import type { EndpointChange } from "src/shared/payload/webhooks";
|
|
||||||
import { getLogger } from "src/utils/logger";
|
|
||||||
|
|
||||||
export class ContextCache {
|
|
||||||
private initialized = false;
|
|
||||||
private logger = getLogger("[ContextCache]");
|
|
||||||
|
|
||||||
constructor(private readonly payload: PayloadSDK) {}
|
|
||||||
|
|
||||||
languages: EndpointLanguage[] = [];
|
|
||||||
locales: string[] = [];
|
|
||||||
currencies: string[] = [];
|
|
||||||
wordings: EndpointWording[] = [];
|
|
||||||
config: EndpointWebsiteConfig = {
|
|
||||||
home: { folders: [] },
|
|
||||||
timeline: { breaks: [], eras: [], eventCount: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.initialized) return;
|
|
||||||
await this.refreshAll();
|
|
||||||
this.initialized = true;
|
|
||||||
this.logger.log("Init complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshAll() {
|
|
||||||
await this.refreshCurrencies();
|
|
||||||
await this.refreshLocales();
|
|
||||||
await this.refreshWebsiteConfig();
|
|
||||||
await this.refreshWordings();
|
|
||||||
}
|
|
||||||
|
|
||||||
async invalidate(changes: EndpointChange[]) {
|
|
||||||
for (const change of changes) {
|
|
||||||
switch (change.type) {
|
|
||||||
case SDKEndpointNames.getWordings:
|
|
||||||
return await this.refreshWordings();
|
|
||||||
|
|
||||||
case SDKEndpointNames.getLanguages:
|
|
||||||
return await this.refreshLocales();
|
|
||||||
|
|
||||||
case SDKEndpointNames.getCurrencies:
|
|
||||||
return await this.refreshCurrencies();
|
|
||||||
|
|
||||||
case SDKEndpointNames.getWebsiteConfig:
|
|
||||||
return await this.refreshWebsiteConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshWordings() {
|
|
||||||
this.wordings = (await this.payload.getWordings()).data;
|
|
||||||
this.logger.log("Wordings refreshed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshCurrencies() {
|
|
||||||
this.currencies = (await this.payload.getCurrencies()).data.map(({ id }) => id);
|
|
||||||
this.logger.log("Currencies refreshed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshLocales() {
|
|
||||||
this.languages = (await this.payload.getLanguages()).data;
|
|
||||||
this.locales = this.languages.filter(({ selectable }) => selectable).map(({ id }) => id);
|
|
||||||
this.logger.log("Locales refreshed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshWebsiteConfig() {
|
|
||||||
this.config = (await this.payload.getWebsiteConfig()).data;
|
|
||||||
this.logger.log("WebsiteConfig refreshed");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
import { getLogger } from "src/utils/logger";
|
|
||||||
import { writeFile, mkdir, readFile } from "fs/promises";
|
|
||||||
import { existsSync } from "fs";
|
|
||||||
import type { PayloadSDK } from "src/shared/payload/sdk";
|
|
||||||
import type { EndpointChange } from "src/shared/payload/webhooks";
|
|
||||||
|
|
||||||
const ON_DISK_ROOT = `.cache/dataCache`;
|
|
||||||
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
|
|
||||||
|
|
||||||
export class DataCache {
|
|
||||||
private readonly logger = getLogger("[DataCache]");
|
|
||||||
private initialized = false;
|
|
||||||
|
|
||||||
private readonly responseCache = new Map<string, any>();
|
|
||||||
|
|
||||||
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly payload: PayloadSDK,
|
|
||||||
private readonly uncachedPayload: PayloadSDK
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.initialized) return;
|
|
||||||
|
|
||||||
if (import.meta.env.DATA_PRECACHING === "true") {
|
|
||||||
await this.precache();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async precache() {
|
|
||||||
// Get all documents from CMS
|
|
||||||
const allDocs = (await this.uncachedPayload.getAll()).data;
|
|
||||||
|
|
||||||
// Load cache from disk if available
|
|
||||||
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
|
|
||||||
this.logger.log("Loading cache from disk...");
|
|
||||||
const buffer = await readFile(ON_DISK_RESPONSE_CACHE_FILE);
|
|
||||||
const data = JSON.parse(buffer.toString()) as [string, any][];
|
|
||||||
for (const [key, value] of data) {
|
|
||||||
// Do not include cache where the key is no longer in the CMS
|
|
||||||
if (!allDocs.find(({ url }) => url === key)) continue;
|
|
||||||
this.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheSizeBeforePrecaching = this.responseCache.size;
|
|
||||||
|
|
||||||
for (const doc of allDocs) {
|
|
||||||
// Do not precache response if already included in the loaded cache from disk
|
|
||||||
if (this.responseCache.has(doc.url)) continue;
|
|
||||||
try {
|
|
||||||
await this.payload.request(doc.url);
|
|
||||||
} catch {
|
|
||||||
this.logger.warn("Precaching failed for url", doc.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheSizeBeforePrecaching !== this.responseCache.size) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log("Precaching completed!", this.responseCache.size, "responses cached");
|
|
||||||
}
|
|
||||||
|
|
||||||
get(url: string) {
|
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
|
||||||
const cachedResponse = this.responseCache.get(url);
|
|
||||||
if (cachedResponse) {
|
|
||||||
this.logger.log("Retrieved cached response for", url);
|
|
||||||
return structuredClone(cachedResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set(url: string, response: any) {
|
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
|
||||||
this.responseCache.set(url, response);
|
|
||||||
this.logger.log("Cached response for", url);
|
|
||||||
if (this.initialized) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async invalidate(changes: EndpointChange[]) {
|
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
|
||||||
|
|
||||||
const urls = changes.map(({ url }) => url);
|
|
||||||
for (const url of urls) {
|
|
||||||
this.responseCache.delete(url);
|
|
||||||
this.logger.log("Invalidated cache for", url);
|
|
||||||
try {
|
|
||||||
await this.payload.request(url);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log("Revalidation fails for", url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.logger.log("There are currently", this.responseCache.size, "responses in cache.");
|
|
||||||
if (this.initialized) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleSave() {
|
|
||||||
if (this.scheduleSaveTimeout) {
|
|
||||||
clearTimeout(this.scheduleSaveTimeout);
|
|
||||||
}
|
|
||||||
this.scheduleSaveTimeout = setTimeout(() => {
|
|
||||||
this.save();
|
|
||||||
}, 10_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async save() {
|
|
||||||
if (!existsSync(ON_DISK_ROOT)) {
|
|
||||||
await mkdir(ON_DISK_ROOT, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializedResponseCache = JSON.stringify([...this.responseCache]);
|
|
||||||
await writeFile(ON_DISK_RESPONSE_CACHE_FILE, serializedResponseCache, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
this.logger.log("Saved", ON_DISK_RESPONSE_CACHE_FILE);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,235 +0,0 @@
|
||||||
import { getLogger } from "src/utils/logger";
|
|
||||||
import { writeFile, mkdir, readFile } from "fs/promises";
|
|
||||||
import { existsSync } from "fs";
|
|
||||||
import {
|
|
||||||
deserializeResponse,
|
|
||||||
serializeResponse,
|
|
||||||
type SerializableResponse,
|
|
||||||
} from "src/utils/responses";
|
|
||||||
import { SDKEndpointNames, type PayloadSDK } from "src/shared/payload/sdk";
|
|
||||||
import type { EndpointChange } from "src/shared/payload/webhooks";
|
|
||||||
import type { ContextCache } from "src/cache/contextCache";
|
|
||||||
|
|
||||||
const ON_DISK_ROOT = `.cache/pageCache`;
|
|
||||||
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
|
|
||||||
|
|
||||||
export class PageCache {
|
|
||||||
private readonly logger = getLogger("[PageCache]");
|
|
||||||
private initialized = false;
|
|
||||||
|
|
||||||
private responseCache = new Map<string, Response>();
|
|
||||||
|
|
||||||
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly uncachedPayload: PayloadSDK,
|
|
||||||
private readonly contextCache: ContextCache
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get(url: string): Response | undefined {
|
|
||||||
if (import.meta.env.PAGE_CACHING !== "true") return;
|
|
||||||
const cachedPage = this.responseCache.get(url);
|
|
||||||
if (cachedPage) {
|
|
||||||
this.logger.log("Retrieved cached page for", url);
|
|
||||||
return cachedPage?.clone();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(url: string, response: Response) {
|
|
||||||
if (import.meta.env.PAGE_CACHING !== "true") return;
|
|
||||||
this.responseCache.set(url, response.clone());
|
|
||||||
this.logger.log("Cached response for", url);
|
|
||||||
if (this.initialized) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.initialized) return;
|
|
||||||
|
|
||||||
if (import.meta.env.PAGE_PRECACHING === "true") {
|
|
||||||
await this.precache();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async precache() {
|
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
|
||||||
|
|
||||||
const allPageUrls = await this.getAllUrls();
|
|
||||||
|
|
||||||
// Load cache from disk if available
|
|
||||||
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
|
|
||||||
this.logger.log("Loading cache from disk...");
|
|
||||||
|
|
||||||
const buffer = await readFile(ON_DISK_RESPONSE_CACHE_FILE);
|
|
||||||
const data = JSON.parse(buffer.toString()) as [string, SerializableResponse][];
|
|
||||||
let deserializedData = data.map<[string, Response]>(([key, value]) => [
|
|
||||||
key,
|
|
||||||
deserializeResponse(value),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Do not include cache where the key is no longer in the CMS
|
|
||||||
deserializedData = deserializedData.filter(([key]) => allPageUrls.has(key));
|
|
||||||
|
|
||||||
this.responseCache = new Map(deserializedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheSizeBeforePrecaching = this.responseCache.size;
|
|
||||||
|
|
||||||
for (const url of allPageUrls) {
|
|
||||||
// Do not precache response if already included in the loaded cache from disk
|
|
||||||
if (this.responseCache.has(url)) continue;
|
|
||||||
try {
|
|
||||||
await fetch(`http://${import.meta.env.ASTRO_HOST}:${import.meta.env.ASTRO_PORT}${url}`);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn("Precaching failed for page", url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheSizeBeforePrecaching !== this.responseCache.size) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log("Precaching completed!", this.responseCache.size, "responses cached");
|
|
||||||
}
|
|
||||||
|
|
||||||
async invalidate(changes: EndpointChange[]) {
|
|
||||||
if (import.meta.env.PAGE_CACHING !== "true") return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
changes.some(({ type }) =>
|
|
||||||
[
|
|
||||||
SDKEndpointNames.getWebsiteConfig,
|
|
||||||
SDKEndpointNames.getLanguages,
|
|
||||||
SDKEndpointNames.getCurrencies,
|
|
||||||
SDKEndpointNames.getWordings,
|
|
||||||
].includes(type)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await this.resetAllUrls();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagesToInvalidate = new Set<string>(
|
|
||||||
changes.flatMap((change) => this.getUrlFromEndpointChange(change))
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const url of pagesToInvalidate) {
|
|
||||||
this.responseCache.delete(url);
|
|
||||||
this.logger.log("Invalidated cache for", url);
|
|
||||||
try {
|
|
||||||
await fetch(`http://${import.meta.env.ASTRO_HOST}:${import.meta.env.ASTRO_PORT}${url}`);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log("Revalidation fails for", url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log("There are currently", this.responseCache.size, "responses in cache.");
|
|
||||||
if (this.initialized) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resetAllUrls() {
|
|
||||||
const allUrls = await this.getAllUrls();
|
|
||||||
this.responseCache.clear();
|
|
||||||
|
|
||||||
for (const url of allUrls) {
|
|
||||||
try {
|
|
||||||
await fetch(`http://${import.meta.env.ASTRO_HOST}:${import.meta.env.ASTRO_PORT}${url}`);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn("Reset failed for page", url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.initialized) {
|
|
||||||
this.scheduleSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleSave() {
|
|
||||||
if (this.scheduleSaveTimeout) {
|
|
||||||
clearTimeout(this.scheduleSaveTimeout);
|
|
||||||
}
|
|
||||||
this.scheduleSaveTimeout = setTimeout(() => {
|
|
||||||
this.save();
|
|
||||||
}, 10_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async save() {
|
|
||||||
if (!existsSync(ON_DISK_ROOT)) {
|
|
||||||
await mkdir(ON_DISK_ROOT, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializedResponses = await Promise.all(
|
|
||||||
[...this.responseCache].map(async ([key, value]) => [key, await serializeResponse(value)])
|
|
||||||
);
|
|
||||||
const serializedResponseCache = JSON.stringify(serializedResponses);
|
|
||||||
await writeFile(ON_DISK_RESPONSE_CACHE_FILE, serializedResponseCache, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
this.logger.log("Saved", ON_DISK_RESPONSE_CACHE_FILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
private getLocalizedUrlsFromUrl = (urls: string[]) => {
|
|
||||||
return urls.flatMap((url) => this.contextCache.locales.map((id) => `/${id}${url}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
private async getAllUrls() {
|
|
||||||
const allChanges = (await this.uncachedPayload.getAll()).data;
|
|
||||||
return new Set<string>([
|
|
||||||
...this.getLocalizedUrlsFromUrl(["", "/settings", "/search"]),
|
|
||||||
...allChanges.flatMap((change) => this.getUrlFromEndpointChange(change)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUrlFromEndpointChange(change: EndpointChange): string[] {
|
|
||||||
const getUnlocalizedUrl = (): string[] => {
|
|
||||||
switch (change.type) {
|
|
||||||
case SDKEndpointNames.getFolder:
|
|
||||||
return [`/folders/${change.slug}`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getCollectible:
|
|
||||||
return [`/collectibles/${change.slug}`, `/collectibles/${change.slug}/relations`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getCollectibleGallery:
|
|
||||||
return [`/collectibles/${change.slug}/gallery`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getCollectibleScans:
|
|
||||||
return [`/collectibles/${change.slug}/scans`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getPage:
|
|
||||||
return [`/pages/${change.slug}`, `/pages/${change.slug}/relations`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getAudioByID:
|
|
||||||
return [`/audios/${change.id}`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getImageByID:
|
|
||||||
return [`/images/${change.id}`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getVideoByID:
|
|
||||||
return [`/videos/${change.id}`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getFileByID:
|
|
||||||
return [`/files/${change.id}`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getRecorderByID:
|
|
||||||
return [`/recorders/${change.id}`];
|
|
||||||
|
|
||||||
case SDKEndpointNames.getChronologyEvents:
|
|
||||||
case SDKEndpointNames.getChronologyEventByID:
|
|
||||||
return [`/timeline`];
|
|
||||||
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getLocalizedUrlsFromUrl(getUnlocalizedUrl());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
export class TokenCache {
|
|
||||||
private token: string | undefined;
|
|
||||||
private expiration: number | undefined;
|
|
||||||
|
|
||||||
get() {
|
|
||||||
if (!this.token) return undefined;
|
|
||||||
if (!this.expiration || this.expiration < Date.now()) {
|
|
||||||
console.log("[PayloadSDK] No token to be retrieved or the token expired");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(newToken: string, newExpiration: number) {
|
|
||||||
this.token = newToken;
|
|
||||||
this.expiration = newExpiration * 1000;
|
|
||||||
const diffInMinutes = Math.floor((this.expiration - Date.now()) / 1000 / 60);
|
|
||||||
console.log("[PayloadSDK] New token set. TTL is", diffInMinutes, "minutes.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,22 +2,25 @@
|
||||||
import Html from "./components/Html.astro";
|
import Html from "./components/Html.astro";
|
||||||
import Topbar from "./components/Topbar/Topbar.astro";
|
import Topbar from "./components/Topbar/Topbar.astro";
|
||||||
import Footer from "./components/Footer.astro";
|
import Footer from "./components/Footer.astro";
|
||||||
|
import type { EndpointSource } from "src/shared/payload/payload-sdk";
|
||||||
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
|
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
|
||||||
import type { ComponentProps } from "astro/types";
|
import type { ComponentProps } from "astro/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
openGraph?: ComponentProps<typeof Html>["openGraph"];
|
openGraph?: ComponentProps<typeof Html>["openGraph"];
|
||||||
|
parentPages?: EndpointSource[];
|
||||||
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
|
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
|
||||||
hideFooterLinks?: boolean;
|
hideFooterLinks?: boolean;
|
||||||
|
hideHomeButton?: boolean;
|
||||||
class?: string | undefined;
|
class?: string | undefined;
|
||||||
topBar?: ComponentProps<typeof Topbar>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
openGraph,
|
openGraph,
|
||||||
|
parentPages,
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
hideFooterLinks = false,
|
hideFooterLinks = false,
|
||||||
topBar = {},
|
hideHomeButton = false,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
---
|
---
|
||||||
|
@ -27,7 +30,7 @@ const {
|
||||||
<Html openGraph={openGraph}>
|
<Html openGraph={openGraph}>
|
||||||
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
|
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
|
||||||
<header>
|
<header>
|
||||||
<Topbar {...topBar} />
|
<Topbar parentPages={parentPages} hideHomeButton={hideHomeButton} />
|
||||||
</header>
|
</header>
|
||||||
<main {...otherProps.class ? otherProps : {}}><slot /></main>
|
<main {...otherProps.class ? otherProps : {}}><slot /></main>
|
||||||
<Footer withLinks={!hideFooterLinks} />
|
<Footer withLinks={!hideFooterLinks} />
|
||||||
|
|
|
@ -30,32 +30,20 @@ const { reducedAsideWidth = false } = Astro.props;
|
||||||
<div id="layout" class:list={{ "reduced-width": reducedAsideWidth }}>
|
<div id="layout" class:list={{ "reduced-width": reducedAsideWidth }}>
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
{
|
<div class="when-not-large">
|
||||||
(Astro.slots.has("header-aside") || Astro.slots.has("meta") || Astro.slots.has("aside")) && (
|
<slot name="header-aside" />
|
||||||
<div class="when-not-large">
|
<slot name="meta" />
|
||||||
<slot name="header-aside" />
|
<slot name="aside" />
|
||||||
<slot name="meta" />
|
</div>
|
||||||
<slot name="aside" />
|
<div class="when-large">
|
||||||
</div>
|
<slot name="meta" />
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
{
|
|
||||||
Astro.slots.has("meta") && (
|
|
||||||
<div class="when-large">
|
|
||||||
<slot name="meta" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{
|
<Card class="when-large right">
|
||||||
(Astro.slots.has("header-aside") || Astro.slots.has("aside")) && (
|
<slot name="header-aside" />
|
||||||
<Card class="when-large right">
|
<slot name="aside" />
|
||||||
<slot name="header-aside" />
|
</Card>
|
||||||
<slot name="aside" />
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import { getRandomId } from "src/utils/random";
|
|
||||||
import { sizesToSrcset } from "src/utils/img";
|
|
||||||
import type {
|
import type {
|
||||||
EndpointImage,
|
EndpointImage,
|
||||||
EndpointPayloadImage,
|
EndpointMediaThumbnail,
|
||||||
EndpointScanImage,
|
EndpointScanImage,
|
||||||
} from "src/shared/payload/endpoint-types";
|
} from "src/shared/payload/payload-sdk";
|
||||||
|
import { getRandomId } from "src/utils/random";
|
||||||
|
import { sizesToSrcset } from "src/utils/img";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
img: EndpointImage | EndpointPayloadImage | EndpointScanImage;
|
img: EndpointImage | EndpointMediaThumbnail | EndpointScanImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -100,7 +100,6 @@ const uniqueId = getRandomId();
|
||||||
const img = document.getElementById(uniqueId);
|
const img = document.getElementById(uniqueId);
|
||||||
const parent = img.parentElement;
|
const parent = img.parentElement;
|
||||||
if (!parent || !img) return;
|
if (!parent || !img) return;
|
||||||
if (img.complete) return;
|
|
||||||
|
|
||||||
parent.style.animationPlayState = "paused";
|
parent.style.animationPlayState = "paused";
|
||||||
img.addEventListener(
|
img.addEventListener(
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { withLinks } = Astro.props;
|
const { withLinks } = Astro.props;
|
||||||
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
const discordLabel = `${t("footer.socials.discord.title")} - ${t(
|
const discordLabel = `${t("footer.socials.discord.title")} - ${t(
|
||||||
"footer.socials.discord.subtitle"
|
"footer.socials.discord.subtitle"
|
||||||
|
@ -30,11 +30,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
|
||||||
{
|
{
|
||||||
withLinks && (
|
withLinks && (
|
||||||
<div id="socials" class="when-no-print">
|
<div id="socials" class="when-no-print">
|
||||||
<a
|
<a href="/discord" class="pressable-label" aria-label={discordLabel} title={discordLabel}>
|
||||||
href={getLocalizedUrl("/discord")}
|
|
||||||
class="pressable-label"
|
|
||||||
aria-label={discordLabel}
|
|
||||||
title={discordLabel}>
|
|
||||||
<Icon name="discord-brands" />
|
<Icon name="discord-brands" />
|
||||||
<p class="font-s">{t("footer.socials.discord.title")}</p>
|
<p class="font-s">{t("footer.socials.discord.title")}</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -105,7 +101,6 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
gap: 0.2em 1em;
|
gap: 0.2em 1em;
|
||||||
place-content: start;
|
place-content: start;
|
||||||
place-items: start;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
@media (min-width: 720.5px) {
|
@media (min-width: 720.5px) {
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
---
|
---
|
||||||
import { UAParser } from "ua-parser-js";
|
import UAParser from "ua-parser-js";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { contextCache } from "src/services";
|
|
||||||
import { PostProcessingTags } from "src/middleware/postProcessing";
|
|
||||||
import type {
|
import type {
|
||||||
EndpointAudio,
|
EndpointAudio,
|
||||||
EndpointImage,
|
EndpointImage,
|
||||||
EndpointPayloadImage,
|
EndpointMediaThumbnail,
|
||||||
EndpointVideo,
|
EndpointVideo,
|
||||||
} from "src/shared/payload/endpoint-types";
|
} from "src/shared/payload/payload-sdk";
|
||||||
|
import { cache } from "src/utils/payload";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
openGraph?:
|
openGraph?:
|
||||||
| {
|
| {
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
thumbnail?: EndpointImage | EndpointPayloadImage | undefined;
|
thumbnail?: EndpointImage | EndpointMediaThumbnail | undefined;
|
||||||
audio?: EndpointAudio | undefined;
|
audio?: EndpointAudio | undefined;
|
||||||
video?: EndpointVideo | undefined;
|
video?: EndpointVideo | undefined;
|
||||||
}
|
}
|
||||||
|
@ -29,9 +28,7 @@ const { openGraph = {} } = Astro.props;
|
||||||
const { description = t("global.meta.description"), audio, video } = openGraph;
|
const { description = t("global.meta.description"), audio, video } = openGraph;
|
||||||
|
|
||||||
const thumbnail =
|
const thumbnail =
|
||||||
openGraph.thumbnail?.openGraph ??
|
openGraph.thumbnail?.openGraph ?? openGraph.thumbnail ?? cache.config.defaultOpenGraphImage;
|
||||||
openGraph.thumbnail ??
|
|
||||||
contextCache.config.defaultOpenGraphImage;
|
|
||||||
|
|
||||||
const title = openGraph.title
|
const title = openGraph.title
|
||||||
? `${openGraph.title} – ${t("global.siteName")}`
|
? `${openGraph.title} – ${t("global.siteName")}`
|
||||||
|
@ -41,6 +38,8 @@ const userAgent = Astro.request.headers.get("user-agent") ?? "";
|
||||||
const parser = new UAParser(userAgent);
|
const parser = new UAParser(userAgent);
|
||||||
const isIOS = parser.getOS().name === "iOS";
|
const isIOS = parser.getOS().name === "iOS";
|
||||||
|
|
||||||
|
const { currentTheme } = Astro.locals;
|
||||||
|
|
||||||
/* Keep that separator here or else it breaks the HTML
|
/* Keep that separator here or else it breaks the HTML
|
||||||
----------------------------------------------- HTML -------------------------------------------- */
|
----------------------------------------------- HTML -------------------------------------------- */
|
||||||
---
|
---
|
||||||
|
@ -48,7 +47,9 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
<html
|
<html
|
||||||
lang={currentLocale}
|
lang={currentLocale}
|
||||||
class:list={{
|
class:list={{
|
||||||
[PostProcessingTags.HTML_CLASS]: true,
|
"manual-theme": currentTheme !== "auto",
|
||||||
|
"light-theme": currentTheme === "light",
|
||||||
|
"dark-theme": currentTheme === "dark",
|
||||||
"texture-dots": !isIOS,
|
"texture-dots": !isIOS,
|
||||||
"font-m": true,
|
"font-m": true,
|
||||||
"debug-lang": false,
|
"debug-lang": false,
|
||||||
|
@ -58,6 +59,8 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/tippy.css" />
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
|
@ -72,8 +75,6 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/tippy.css" />
|
|
||||||
|
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fdebd4" />
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fdebd4" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27231e" />
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27231e" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
@ -130,6 +131,16 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<style is:global>
|
||||||
|
.when-js {
|
||||||
|
display: none !important;
|
||||||
|
visibility: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</noscript>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -299,7 +310,7 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
color: var(--color-base-1000);
|
color: var(--color-base-1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not(.light-theme, .dark-theme) {
|
html:not(.manual-theme) {
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
& .when-dark-theme {
|
& .when-dark-theme {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
@ -674,14 +685,10 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 0.4em;
|
gap: 0.4em;
|
||||||
padding: 0.7em 1.1em;
|
padding: 0.7em 0.8em;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:has(svg) {
|
|
||||||
padding-left: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: 150ms background-color;
|
transition: 150ms background-color;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
@ -809,26 +816,14 @@ const isIOS = parser.getOS().name === "iOS";
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (scripting: none) {
|
|
||||||
.when-js {
|
|
||||||
display: none !important;
|
|
||||||
visibility: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (scripting: enabled) {
|
|
||||||
.when-no-js {
|
|
||||||
display: none !important;
|
|
||||||
visibility: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{/* ------------------------------------------- JS --------------------------------------------- */}
|
{/* ------------------------------------------- JS --------------------------------------------- */}
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
Array.from(document.querySelectorAll(".when-no-js")).forEach((node) => node.remove());
|
||||||
|
</script>
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
document.querySelectorAll("a").forEach((element) => {
|
document.querySelectorAll("a").forEach((element) => {
|
||||||
const href = element.getAttribute("href");
|
const href = element.getAttribute("href");
|
||||||
|
|
|
@ -4,23 +4,16 @@ import Button from "components/Button.astro";
|
||||||
import ThemeSelector from "./components/ThemeSelector.astro";
|
import ThemeSelector from "./components/ThemeSelector.astro";
|
||||||
import LanguageSelector from "./components/LanguageSelector.astro";
|
import LanguageSelector from "./components/LanguageSelector.astro";
|
||||||
import CurrencySelector from "./components/CurrencySelector.astro";
|
import CurrencySelector from "./components/CurrencySelector.astro";
|
||||||
import RelationsButton from "./components/RelationsButton.astro";
|
import ParentPagesButton from "./components/ParentPagesButton.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
import type { EndpointSource } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
relations?: EndpointRelation[] | undefined;
|
parentPages?: EndpointSource[] | undefined;
|
||||||
relationPageUrl?: string | undefined;
|
hideHomeButton?: boolean;
|
||||||
hideHomeButton?: boolean | undefined;
|
|
||||||
hideSearchButton?: boolean | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { parentPages = [], hideHomeButton = false } = Astro.props;
|
||||||
relations = [],
|
|
||||||
relationPageUrl,
|
|
||||||
hideHomeButton = false,
|
|
||||||
hideSearchButton = false,
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
---
|
---
|
||||||
|
@ -29,31 +22,23 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
<nav id="topbar" class="when-no-print">
|
<nav id="topbar" class="when-no-print">
|
||||||
{
|
{
|
||||||
(!hideHomeButton || relations.length > 0) && (
|
(!hideHomeButton || parentPages.length > 0) && (
|
||||||
<div id="left" class="hide-scrollbar">
|
<div id="left" class="hide-scrollbar">
|
||||||
<a href={getLocalizedUrl("")} class="pressable-label">
|
<a href={getLocalizedUrl("/")} class="pressable-label">
|
||||||
<Icon name="material-symbols:home" width={16} height={16} />
|
<Icon name="material-symbols:home" width={16} height={16} />
|
||||||
<p>{t("home.title")}</p>
|
<p>{t("home.title")}</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{relations.length > 0 && (
|
{parentPages.length > 0 && <ParentPagesButton parentPages={parentPages} />}
|
||||||
<RelationsButton relations={relations} relationPageUrl={relationPageUrl} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<div id="toolbar" class="hide-scrollbar">
|
<div id="toolbar" class="hide-scrollbar">
|
||||||
{
|
<a href={getLocalizedUrl("/search")} aria-label={t("header.topbar.search.tooltip")} hidden>
|
||||||
!hideSearchButton && (
|
<Button icon="material-symbols:search" />
|
||||||
<>
|
</a>
|
||||||
<a href={getLocalizedUrl("/search")} title={t("header.topbar.search.tooltip")}>
|
<div class="separator" hidden></div>
|
||||||
<Button icon="material-symbols:search" />
|
|
||||||
</a>
|
|
||||||
<div class="separator" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="when-no-js">
|
<div class="when-no-js">
|
||||||
<a href="/settings">
|
<a href="/settings">
|
||||||
|
|
|
@ -2,12 +2,8 @@
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { contextCache } from "src/services";
|
import { cache } from "src/utils/payload";
|
||||||
import { formatCurrency } from "src/utils/currencies";
|
import { formatCurrency } from "src/utils/currencies";
|
||||||
import {
|
|
||||||
PostProcessingTags,
|
|
||||||
prepareClassForSelectedCurrencyPostProcessing,
|
|
||||||
} from "src/middleware/postProcessing";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
withTitle?: boolean | undefined;
|
withTitle?: boolean | undefined;
|
||||||
|
@ -17,12 +13,7 @@ interface Props {
|
||||||
const { withTitle, ...otherProps } = Astro.props;
|
const { withTitle, ...otherProps } = Astro.props;
|
||||||
const { t } = await getI18n(Astro.locals.currentLocale);
|
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
const getActionCurrency = (value: string) => {
|
const { currentCurrency } = Astro.locals;
|
||||||
const requestSearchParams = new URL(Astro.request.url).searchParams;
|
|
||||||
const newSearchParams = new URLSearchParams(requestSearchParams);
|
|
||||||
newSearchParams.set("action-currency", value);
|
|
||||||
return `?${newSearchParams}`;
|
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
@ -30,13 +21,14 @@ const getActionCurrency = (value: string) => {
|
||||||
<Tooltip trigger="click" {...otherProps.class ? otherProps : {}}>
|
<Tooltip trigger="click" {...otherProps.class ? otherProps : {}}>
|
||||||
<div id="content" slot="tooltip-content">
|
<div id="content" slot="tooltip-content">
|
||||||
{
|
{
|
||||||
contextCache.currencies.map((id) => (
|
cache.currencies.map((id) => (
|
||||||
<a
|
<a
|
||||||
class:list={[
|
class:list={{
|
||||||
"pressable-link",
|
current: currentCurrency === id,
|
||||||
prepareClassForSelectedCurrencyPostProcessing({ currency: id }),
|
"pressable-link": true,
|
||||||
]}
|
}}
|
||||||
href={getActionCurrency(id)}>
|
href={`?action-currency=${id}`}
|
||||||
|
data-astro-prefetch="tap">
|
||||||
{`${id} (${formatCurrency(id)})`}
|
{`${id} (${formatCurrency(id)})`}
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
|
@ -44,7 +36,7 @@ const getActionCurrency = (value: string) => {
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
icon="material-symbols:currency-exchange"
|
icon="material-symbols:currency-exchange"
|
||||||
title={withTitle ? PostProcessingTags.PREFERRED_CURRENCY : undefined}
|
title={withTitle ? currentCurrency.toUpperCase() : undefined}
|
||||||
ariaLabel={t("header.topbar.currency.tooltip")}
|
ariaLabel={t("header.topbar.currency.tooltip")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { contextCache } from "src/services";
|
import { cache } from "src/utils/payload";
|
||||||
import { formatLocale } from "src/utils/format";
|
import { formatLocale } from "src/utils/format";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -14,13 +14,6 @@ const { withTitle, ...otherProps } = Astro.props;
|
||||||
|
|
||||||
const { currentLocale } = Astro.locals;
|
const { currentLocale } = Astro.locals;
|
||||||
const { t } = await getI18n(currentLocale);
|
const { t } = await getI18n(currentLocale);
|
||||||
|
|
||||||
const getActionLanguage = (value: string) => {
|
|
||||||
const requestSearchParams = new URL(Astro.request.url).searchParams;
|
|
||||||
const newSearchParams = new URLSearchParams(requestSearchParams);
|
|
||||||
newSearchParams.set("action-lang", value);
|
|
||||||
return `?${newSearchParams}`;
|
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
@ -28,11 +21,12 @@ const getActionLanguage = (value: string) => {
|
||||||
<Tooltip trigger="click" {...otherProps.class ? otherProps : {}}>
|
<Tooltip trigger="click" {...otherProps.class ? otherProps : {}}>
|
||||||
<div id="content" slot="tooltip-content">
|
<div id="content" slot="tooltip-content">
|
||||||
{
|
{
|
||||||
contextCache.locales.map((locale) => (
|
cache.locales.map(({ id }) => (
|
||||||
<a
|
<a
|
||||||
class:list={{ current: currentLocale === locale, "pressable-link": true }}
|
class:list={{ current: currentLocale === id, "pressable-link": true }}
|
||||||
href={getActionLanguage(locale)}>
|
href={`?action-lang=${id}`}
|
||||||
{formatLocale(locale)}
|
data-astro-prefetch="tap">
|
||||||
|
{formatLocale(id)}
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
import Tooltip from "components/Tooltip.astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
import ReturnToButton from "./ReturnToButton.astro";
|
||||||
|
import type { EndpointSource } from "src/shared/payload/payload-sdk";
|
||||||
|
import SourceRow from "components/SourceRow.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
parentPages: EndpointSource[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parentPages } = Astro.props;
|
||||||
|
|
||||||
|
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
|
{
|
||||||
|
parentPages.length === 1 && parentPages[0] ? (
|
||||||
|
<ReturnToButton parentPage={parentPages[0]} />
|
||||||
|
) : (
|
||||||
|
<Tooltip trigger="click" class="when-js">
|
||||||
|
<div id="tooltip-content" slot="tooltip-content">
|
||||||
|
<p>{t("header.nav.parentPages.tooltip")}</p>
|
||||||
|
<div>
|
||||||
|
{parentPages.map((parentPage) => (
|
||||||
|
<SourceRow source={parentPage} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="pressable-label">
|
||||||
|
<Icon name="material-symbols:keyboard-return" />
|
||||||
|
<p>
|
||||||
|
{t("header.nav.parentPages.label", {
|
||||||
|
count: parentPages.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#tooltip-content {
|
||||||
|
> p {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,87 +0,0 @@
|
||||||
---
|
|
||||||
import Tooltip from "components/Tooltip.astro";
|
|
||||||
import { Icon } from "astro-icon/components";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import ReturnToButton from "./ReturnToButton.astro";
|
|
||||||
import RelationRow from "components/RelationRow.astro";
|
|
||||||
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
relations: EndpointRelation[];
|
|
||||||
relationPageUrl?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { relations, relationPageUrl } = Astro.props;
|
|
||||||
|
|
||||||
const { t } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
|
|
||||||
const buttonLabel = t("header.nav.parentPages.label", {
|
|
||||||
count: relations.length,
|
|
||||||
});
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
{
|
|
||||||
relations.length === 1 && relations[0] ? (
|
|
||||||
<ReturnToButton relation={relations[0]} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Tooltip trigger="click" class="when-js">
|
|
||||||
<div id="tooltip-content" slot="tooltip-content">
|
|
||||||
<p>{t("header.nav.parentPages.tooltip")}</p>
|
|
||||||
<div>
|
|
||||||
{relations.slice(0, 5).map((relation) => (
|
|
||||||
<RelationRow relation={relation} />
|
|
||||||
))}
|
|
||||||
{relationPageUrl && (
|
|
||||||
<>
|
|
||||||
<hr />
|
|
||||||
<a href={relationPageUrl} class="pressable-link">
|
|
||||||
{t("header.nav.parentPages.tooltip.viewAll")}
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pressable-label">
|
|
||||||
<Icon name="material-symbols:keyboard-return" />
|
|
||||||
<p>{buttonLabel}</p>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{relationPageUrl && (
|
|
||||||
<a class="pressable-label when-no-js" href={relationPageUrl}>
|
|
||||||
<Icon name="material-symbols:keyboard-return" />
|
|
||||||
<div>{buttonLabel}</div>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#tooltip-content {
|
|
||||||
> p {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.8em;
|
|
||||||
|
|
||||||
& > hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 2px dotted var(--color-base-500);
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .pressable-link {
|
|
||||||
padding: 4px 10px;
|
|
||||||
margin: -4px -10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
import type { EndpointSource } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
relation: EndpointRelation;
|
parentPage: EndpointSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { relation } = Astro.props;
|
const { parentPage } = Astro.props;
|
||||||
const { formatEndpointRelation } = await getI18n(Astro.locals.currentLocale);
|
const { formatEndpointSource } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
href,
|
href,
|
||||||
|
@ -17,7 +17,7 @@ const {
|
||||||
target = undefined,
|
target = undefined,
|
||||||
rel = undefined,
|
rel = undefined,
|
||||||
lang,
|
lang,
|
||||||
} = formatEndpointRelation(relation);
|
} = formatEndpointSource(parentPage);
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
import Button from "components/Button.astro";
|
|
||||||
import Card from "components/Card.astro";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
parent: EndpointRelation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { parent } = Astro.props;
|
|
||||||
|
|
||||||
const { formatEndpointRelation, t } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
const { href, target, label } = formatEndpointRelation(parent);
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<Card class="card_subpage">
|
|
||||||
<p class="font-l">{t("global.subpageCard.message", { title: label })}</p>
|
|
||||||
<a href={href} target={target}>
|
|
||||||
<Button title={t("global.subpageCard.returnButton")} icon="material-symbols:keyboard-return" />
|
|
||||||
</a>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.card_subpage {
|
|
||||||
padding: 2em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
width: fit-content;
|
|
||||||
place-items: start;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -3,28 +3,27 @@ import Button from "components/Button.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
const { currentLocale } = Astro.locals;
|
const { currentLocale, currentTheme } = Astro.locals;
|
||||||
const { t } = await getI18n(currentLocale);
|
const { t } = await getI18n(currentLocale);
|
||||||
|
|
||||||
const getActionThemeUrl = (value: "dark" | "light" | "auto") => {
|
|
||||||
const requestSearchParams = new URL(Astro.request.url).searchParams;
|
|
||||||
const newSearchParams = new URLSearchParams(requestSearchParams);
|
|
||||||
newSearchParams.set("action-theme", value);
|
|
||||||
return `?${newSearchParams}`;
|
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
<Tooltip trigger="click">
|
<Tooltip trigger="click">
|
||||||
<div id="content" slot="tooltip-content">
|
<div id="content" slot="tooltip-content">
|
||||||
<a class="pressable-link underline-when-dark" href={getActionThemeUrl("dark")}>
|
<a
|
||||||
|
class:list={{ current: currentTheme === "dark", "pressable-link": true }}
|
||||||
|
href="?action-theme=dark">
|
||||||
{t("global.theme.dark")}
|
{t("global.theme.dark")}
|
||||||
</a>
|
</a>
|
||||||
<a class="pressable-link underline-when-auto" href={getActionThemeUrl("auto")}>
|
<a
|
||||||
|
class:list={{ current: currentTheme === "auto", "pressable-link": true }}
|
||||||
|
href="?action-theme=auto">
|
||||||
{t("global.theme.auto")}
|
{t("global.theme.auto")}
|
||||||
</a>
|
</a>
|
||||||
<a class="pressable-link underline-when-light" href={getActionThemeUrl("light")}>
|
<a
|
||||||
|
class:list={{ current: currentTheme === "light", "pressable-link": true }}
|
||||||
|
href="?action-theme=light">
|
||||||
{t("global.theme.light")}
|
{t("global.theme.light")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,12 +45,10 @@ const getActionThemeUrl = (value: "dark" | "light" | "auto") => {
|
||||||
#content {
|
#content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.light-theme) a.underline-when-light,
|
& > .current {
|
||||||
:global(html.dark-theme) a.underline-when-dark,
|
color: var(--color-base-750);
|
||||||
:global(html:not(.light-theme, .dark-theme)) a.underline-when-auto {
|
text-decoration: underline 0.08em var(--color-base-650);
|
||||||
color: var(--color-base-750);
|
}
|
||||||
text-decoration: underline 0.08em var(--color-base-650);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
|
import { AttributeTypes, type EndpointAttribute } from "src/shared/payload/payload-sdk";
|
||||||
import Metadata from "./Metadata.astro";
|
import Metadata from "./Metadata.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import ErrorMessage from "./ErrorMessage.astro";
|
import ErrorMessage from "./ErrorMessage.astro";
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
import type { EndpointAttribute } from "src/shared/payload/endpoint-types";
|
|
||||||
import { AttributeTypes } from "src/shared/payload/constants";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attributes: (EndpointAttribute | Attribute)[];
|
attributes: (EndpointAttribute | Attribute)[];
|
||||||
|
@ -68,7 +67,12 @@ const { getLocalizedMatch, getLocalizedUrl, formatNumber } = await getI18n(
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <ErrorMessage title={`Unknown attribute type: ${type}`} />;
|
return (
|
||||||
|
<ErrorMessage
|
||||||
|
title={`Unknown attribute type: ${type}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import type { EndpointAudioPreview } from "src/shared/payload/endpoint-types";
|
import type { EndpointAudio } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
audio: EndpointAudioPreview;
|
audio: EndpointAudio;
|
||||||
class?: string | undefined;
|
class?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
---
|
---
|
||||||
|
import {
|
||||||
|
isBlockLineBlock,
|
||||||
|
type GenericBlock,
|
||||||
|
isBlockCueBlock,
|
||||||
|
} from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
import LineBlock from "./components/LineBlock.astro";
|
import LineBlock from "./components/LineBlock.astro";
|
||||||
import CueBlock from "./components/CueBlock.astro";
|
import CueBlock from "./components/CueBlock.astro";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
import { type GenericBlock, isBlockLineBlock, isBlockCueBlock } from "src/shared/payload/blocks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
block: GenericBlock;
|
block: GenericBlock;
|
||||||
|
@ -20,6 +25,9 @@ const { block, lang } = Astro.props;
|
||||||
) : isBlockCueBlock(block) ? (
|
) : isBlockCueBlock(block) ? (
|
||||||
<CueBlock block={block} lang={lang} />
|
<CueBlock block={block} lang={lang} />
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown block type: ${block.blockType}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown block type: ${block.blockType}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import RichText from "components/RichText/RichText.astro";
|
import RichText from "components/RichText/RichText.astro";
|
||||||
import type { CueBlock } from "src/shared/payload/blocks";
|
import type { CueBlock } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
block: CueBlock;
|
block: CueBlock;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import RichText from "components/RichText/RichText.astro";
|
import RichText from "components/RichText/RichText.astro";
|
||||||
import type { LineBlock } from "src/shared/payload/blocks";
|
import type { LineBlock } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
block: LineBlock;
|
block: LineBlock;
|
||||||
|
|
|
@ -5,21 +5,20 @@ interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
class?: string;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
class?: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, icon, ariaLabel, id, ...otherProps } = Astro.props;
|
const { title, icon, class: className, ariaLabel, id } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
class:list={["pressable", { "with-title": !!title }, otherProps.class]}
|
class:list={["pressable", { "with-title": !!title }, className]}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
title={ariaLabel}
|
title={ariaLabel}>
|
||||||
{...otherProps.class ? otherProps : {}}>
|
|
||||||
{icon && <Icon name={icon} />}
|
{icon && <Icon name={icon} />}
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import type { EndpointCredit } from "src/shared/payload/endpoint-types";
|
import type { EndpointCredit } from "src/shared/payload/payload-sdk";
|
||||||
import Metadata from "./Metadata.astro";
|
import Metadata from "./Metadata.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
|
@ -30,7 +30,6 @@ const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.curren
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
@ -38,6 +37,6 @@ const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.curren
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1.5em;
|
gap: 2em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,27 +5,18 @@ import Button from "./Button.astro";
|
||||||
interface Props {
|
interface Props {
|
||||||
href: string;
|
href: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
useBlob?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { href, filename, useBlob = false } = Astro.props;
|
const { href, filename } = Astro.props;
|
||||||
|
|
||||||
const { t } = await getI18n(Astro.locals.currentLocale);
|
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
{
|
<download-button href={href} filename={filename} class="when-js when-no-print">
|
||||||
useBlob ? (
|
<Button title={t("global.downloadButton")} icon="material-symbols:download" />
|
||||||
<download-button href={href} filename={filename} class="when-js when-no-print">
|
</download-button>
|
||||||
<Button title={t("global.downloadButton")} icon="material-symbols:download" />
|
|
||||||
</download-button>
|
|
||||||
) : (
|
|
||||||
<a href={href} class="when-no-print">
|
|
||||||
<Button title={t("global.downloadButton")} icon="material-symbols:download" />
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* ------------------------------------------- JS --------------------------------------------- */}
|
{/* ------------------------------------------- JS --------------------------------------------- */}
|
||||||
|
|
||||||
|
@ -41,11 +32,11 @@ const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
elem.addEventListener("click", async () => {
|
elem.addEventListener("click", async () => {
|
||||||
const res = await fetch(href);
|
const res = await fetch(href);
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const url = window.URL.createObjectURL(blob);
|
const blobURL = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
var link = document.createElement("a");
|
var link = document.createElement("a");
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
link.href = url;
|
link.href = blobURL;
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
const { title, description } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -17,19 +14,7 @@ const { title, description } = Astro.props;
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
<Icon name="material-symbols:error-outline" width={32} height={32} />
|
<Icon name="material-symbols:error-outline" width={32} height={32} />
|
||||||
<p class="font-xl">{title}</p>
|
<p class="font-xl">{title}</p>
|
||||||
{
|
{description && <p>{description}</p>}
|
||||||
description ? (
|
|
||||||
<p>{description}</p>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
Please contact{" "}
|
|
||||||
<a href={getLocalizedUrl("/discord")} target="_blank">
|
|
||||||
website technical administrator
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
|
import { AttributeTypes, type EndpointAttribute } from "src/shared/payload/payload-sdk";
|
||||||
import InlineMetadata from "./InlineMetadata.astro";
|
import InlineMetadata from "./InlineMetadata.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import ErrorMessage from "./ErrorMessage.astro";
|
import ErrorMessage from "./ErrorMessage.astro";
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
import { AttributeTypes } from "src/shared/payload/constants";
|
|
||||||
import type { EndpointAttribute } from "src/shared/payload/endpoint-types";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attributes: (EndpointAttribute | Attribute)[];
|
attributes: (EndpointAttribute | Attribute)[];
|
||||||
|
@ -68,7 +67,12 @@ const { getLocalizedMatch, getLocalizedUrl, formatNumber } = await getI18n(
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <ErrorMessage title={`Unknown attribute type: ${type}`} />;
|
return (
|
||||||
|
<ErrorMessage
|
||||||
|
title={`Unknown attribute type: ${type}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
|
import type { EndpointCredit } from "src/shared/payload/payload-sdk";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import InlineMetadata from "./InlineMetadata.astro";
|
import InlineMetadata from "./InlineMetadata.astro";
|
||||||
import type { EndpointCredit } from "src/shared/payload/endpoint-types";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
credits: EndpointCredit[];
|
credits: EndpointCredit[];
|
||||||
|
@ -23,15 +23,17 @@ const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
gap: 2em;
|
||||||
gap: 0.5em;
|
|
||||||
|
@media (max-width: 35rem) {
|
||||||
|
gap: 3em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
---
|
||||||
|
import MasoActor from "components/Maso/MasoActor.astro";
|
||||||
|
import Tooltip from "components/Tooltip.astro";
|
||||||
|
import Button from "components/Button.astro";
|
||||||
|
import { formatLocale } from "src/utils/format";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentLanguage: string;
|
||||||
|
currentSourceLanguage: string;
|
||||||
|
getPartialUrl: (locale: string) => string;
|
||||||
|
availableLanguages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentLanguage, currentSourceLanguage, getPartialUrl, availableLanguages } = Astro.props;
|
||||||
|
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
|
<div id="lang-selector" class="when-js when-no-print">
|
||||||
|
<Tooltip trigger="click">
|
||||||
|
<Button
|
||||||
|
icon="material-symbols:translate"
|
||||||
|
title={`${currentLanguage !== currentSourceLanguage ? `${currentSourceLanguage.toUpperCase()} → ` : ""}${currentLanguage.toUpperCase()}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div id="tooltip-content" slot="tooltip-content">
|
||||||
|
{
|
||||||
|
availableLanguages.map((id) => (
|
||||||
|
<MasoActor href={getPartialUrl(id)}>
|
||||||
|
<p class:list={{ current: id === currentLanguage, "pressable-link": true }}>
|
||||||
|
{formatLocale(id)}
|
||||||
|
</p>
|
||||||
|
</MasoActor>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
t("global.languageOverride.availableLanguages", {
|
||||||
|
count: availableLanguages.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#lang-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
#tooltip-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
& .current {
|
||||||
|
color: var(--color-base-750);
|
||||||
|
text-decoration: underline 0.08em var(--color-base-650);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,11 @@
|
||||||
---
|
---
|
||||||
|
import {
|
||||||
|
type EndpointCredit,
|
||||||
|
type EndpointImage,
|
||||||
|
type EndpointMediaThumbnail,
|
||||||
|
type EndpointScanImage,
|
||||||
|
type RichTextContent,
|
||||||
|
} from "src/shared/payload/payload-sdk";
|
||||||
import Credits from "./Credits.astro";
|
import Credits from "./Credits.astro";
|
||||||
import DownloadButton from "./DownloadButton.astro";
|
import DownloadButton from "./DownloadButton.astro";
|
||||||
import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
|
import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
|
||||||
|
@ -7,18 +14,11 @@ import Attributes from "./Attributes.astro";
|
||||||
import { sizesToSrcset } from "src/utils/img";
|
import { sizesToSrcset } from "src/utils/img";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import RichText from "./RichText/RichText.astro";
|
import RichText from "./RichText/RichText.astro";
|
||||||
import type {
|
|
||||||
EndpointImage,
|
|
||||||
EndpointScanImage,
|
|
||||||
EndpointPayloadImage,
|
|
||||||
EndpointCredit,
|
|
||||||
} from "src/shared/payload/endpoint-types";
|
|
||||||
import type { RichTextContent } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
previousImageHref?: string | undefined;
|
previousImageHref?: string | undefined;
|
||||||
nextImageHref?: string | undefined;
|
nextImageHref?: string | undefined;
|
||||||
image: EndpointImage | EndpointScanImage | EndpointPayloadImage;
|
image: EndpointImage | EndpointScanImage | EndpointMediaThumbnail;
|
||||||
pretitle?: string | undefined;
|
pretitle?: string | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string | undefined;
|
subtitle?: string | undefined;
|
||||||
|
@ -59,7 +59,8 @@ const hasNavigation = previousImageHref || nextImageHref;
|
||||||
<a
|
<a
|
||||||
id="previous-button"
|
id="previous-button"
|
||||||
class:list={{ hidden: !previousImageHref, pressable: true }}
|
class:list={{ hidden: !previousImageHref, pressable: true }}
|
||||||
href={previousImageHref}>
|
href={previousImageHref}
|
||||||
|
data-astro-history="replace">
|
||||||
<Icon name="material-symbols:chevron-left" />
|
<Icon name="material-symbols:chevron-left" />
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
@ -80,7 +81,8 @@ const hasNavigation = previousImageHref || nextImageHref;
|
||||||
<a
|
<a
|
||||||
id="next-button"
|
id="next-button"
|
||||||
class:list={{ hidden: !nextImageHref, pressable: true }}
|
class:list={{ hidden: !nextImageHref, pressable: true }}
|
||||||
href={nextImageHref}>
|
href={nextImageHref}
|
||||||
|
data-astro-history="replace">
|
||||||
<Icon name="material-symbols:chevron-right" />
|
<Icon name="material-symbols:chevron-right" />
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
@ -108,7 +110,7 @@ const hasNavigation = previousImageHref || nextImageHref;
|
||||||
{attributes.length > 0 && <Attributes attributes={attributes} />}
|
{attributes.length > 0 && <Attributes attributes={attributes} />}
|
||||||
{credits.length > 0 && <Credits credits={credits} />}
|
{credits.length > 0 && <Credits credits={credits} />}
|
||||||
{metaAttributes.length > 0 && <Attributes attributes={metaAttributes} />}
|
{metaAttributes.length > 0 && <Attributes attributes={metaAttributes} />}
|
||||||
{filename && <DownloadButton href={url} filename={filename} useBlob />}
|
{filename && <DownloadButton href={url} filename={filename} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,8 @@ const { href, method = "get", class: className, id } = Astro.props;
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media (scripting: enabled) {
|
maso-actor {
|
||||||
maso-actor {
|
cursor: pointer;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
import TitleIcon from "./TitleIcon.astro";
|
import TitleIcon from "./TitleIcon.astro";
|
||||||
import { isExternalLink } from "src/utils/urls";
|
|
||||||
|
|
||||||
interface Props extends Attribute {}
|
interface Props extends Attribute {}
|
||||||
|
|
||||||
|
@ -16,23 +15,15 @@ if (values.length === 0) return;
|
||||||
<TitleIcon title={title} icon={icon} lang={titleLang} />
|
<TitleIcon title={title} icon={icon} lang={titleLang} />
|
||||||
<div id="values" class:list={{ "with-border": withBorder }}>
|
<div id="values" class:list={{ "with-border": withBorder }}>
|
||||||
{
|
{
|
||||||
values.map(({ name, href, lang }) => {
|
values.map(({ name, href, lang }) =>
|
||||||
if (!href) {
|
href ? (
|
||||||
return <div lang={lang}>{name}</div>;
|
<a class="pressable" href={href} lang={lang}>
|
||||||
} else {
|
{name}
|
||||||
const newTab = isExternalLink(href);
|
</a>
|
||||||
return (
|
) : (
|
||||||
<a
|
<div lang={lang}>{name}</div>
|
||||||
class="pressable"
|
)
|
||||||
href={href}
|
)
|
||||||
lang={lang}
|
|
||||||
target={newTab ? "_blank" : undefined}
|
|
||||||
rel={newTab ? "noopener noreferrer" : undefined}>
|
|
||||||
{name}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,7 +41,7 @@ if (values.length === 0) return;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
translate: 0px 2px;
|
translate: 0px 3px;
|
||||||
|
|
||||||
&.with-border {
|
&.with-border {
|
||||||
& > div,
|
& > div,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointAudioPreview } from "src/shared/payload/endpoint-types";
|
import type { EndpointAudio } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
audio: EndpointAudioPreview;
|
audio: EndpointAudio;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLocalizedMatch, getLocalizedUrl, t, formatDuration } = await getI18n(
|
const { getLocalizedMatch, getLocalizedUrl, t, formatDuration } = await getI18n(
|
||||||
|
@ -41,6 +41,6 @@ const attributesWithMeta = [
|
||||||
href={getLocalizedUrl(`/audios/${id}`)}
|
href={getLocalizedUrl(`/audios/${id}`)}
|
||||||
attributes={attributesWithMeta}
|
attributes={attributesWithMeta}
|
||||||
icon="material-symbols:music-note"
|
icon="material-symbols:music-note"
|
||||||
iconHoverLabel={t("global.collections.audios", { count: 1 })}
|
iconHoverLabel={t("global.previewTypes.audio")}
|
||||||
smallTitle={title === filename}
|
smallTitle={title === filename}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
---
|
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import type { EndpointChronologyEvent } from "src/shared/payload/endpoint-types";
|
|
||||||
import { formatRichTextToString, formatTimelineDateToId } from "src/utils/format";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
event: EndpointChronologyEvent["events"][number];
|
|
||||||
date: EndpointChronologyEvent["date"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getLocalizedUrl, getLocalizedMatch, t, formatTimelineDate } = await getI18n(
|
|
||||||
Astro.locals.currentLocale
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
date,
|
|
||||||
event: { translations },
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const { title, description, language } = getLocalizedMatch(translations);
|
|
||||||
|
|
||||||
const getTruncatedText = () => {
|
|
||||||
const getTextContent = () => {
|
|
||||||
if (title) return title;
|
|
||||||
if (description) return formatRichTextToString(description);
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const text = getTextContent();
|
|
||||||
const limit = 45;
|
|
||||||
const truncationMark = "...";
|
|
||||||
if (text.length < limit) return text;
|
|
||||||
return text.substring(0, limit - truncationMark.length) + truncationMark;
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<GenericPreview
|
|
||||||
lang={language}
|
|
||||||
pretitle={formatTimelineDate(date)}
|
|
||||||
title={getTruncatedText()}
|
|
||||||
href={getLocalizedUrl(`/timeline#${formatTimelineDateToId(date)}`)}
|
|
||||||
icon="material-symbols:calendar-month"
|
|
||||||
iconHoverLabel={t("global.collections.chronologyEvents")}
|
|
||||||
smallTitle
|
|
||||||
/>
|
|
|
@ -1,16 +1,16 @@
|
||||||
---
|
---
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { formatPriceForPostProcessing } from "src/middleware/postProcessing";
|
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
|
||||||
import type { EndpointCollectiblePreview } from "src/shared/payload/endpoint-types";
|
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
|
import { convert } from "src/utils/currencies";
|
||||||
import { formatLocale } from "src/utils/format";
|
import { formatLocale } from "src/utils/format";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collectible: EndpointCollectiblePreview;
|
collectible: EndpointCollectible;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLocalizedMatch, getLocalizedUrl, t, formatDate } = await getI18n(
|
const { getLocalizedMatch, getLocalizedUrl, t, formatPrice, formatDate } = await getI18n(
|
||||||
Astro.locals.currentLocale
|
Astro.locals.currentLocale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -41,13 +41,18 @@ if (releaseDate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (price) {
|
if (price) {
|
||||||
|
const preferredCurrency = Astro.locals.currentCurrency;
|
||||||
|
|
||||||
|
const convertedPrice = {
|
||||||
|
amount: convert(price.currency, preferredCurrency, price.amount),
|
||||||
|
currency: preferredCurrency,
|
||||||
|
};
|
||||||
|
|
||||||
additionalAttributes.push({
|
additionalAttributes.push({
|
||||||
title: t("collectibles.price"),
|
title: t("collectibles.price"),
|
||||||
icon: "material-symbols:sell",
|
icon: "material-symbols:sell",
|
||||||
values: [
|
values: [
|
||||||
{
|
{ name: price.amount === 0 ? t("collectibles.price.free") : formatPrice(convertedPrice) },
|
||||||
name: formatPriceForPostProcessing(price, "short"),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
withBorder: false,
|
withBorder: false,
|
||||||
});
|
});
|
||||||
|
@ -65,6 +70,6 @@ if (price) {
|
||||||
href={getLocalizedUrl(`/collectibles/${slug}`)}
|
href={getLocalizedUrl(`/collectibles/${slug}`)}
|
||||||
attributes={[...attributes, ...additionalAttributes]}
|
attributes={[...attributes, ...additionalAttributes]}
|
||||||
icon="material-symbols:category"
|
icon="material-symbols:category"
|
||||||
iconHoverLabel={t("global.collections.collectibles", { count: 1 })}
|
iconHoverLabel={t("global.previewTypes.collectible")}
|
||||||
disableRoundedTop
|
disableRoundedTop
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
---
|
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import type { EndpointFilePreview } from "src/shared/payload/endpoint-types";
|
|
||||||
import { getFileIcon } from "src/utils/attributes";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
file: EndpointFilePreview;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getLocalizedMatch, getLocalizedUrl, t, formatFilesize } = await getI18n(
|
|
||||||
Astro.locals.currentLocale
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
file: { id, translations, attributes, filename, thumbnail, mimeType, filesize },
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const { pretitle, title, subtitle, language } =
|
|
||||||
translations.length > 0
|
|
||||||
? getLocalizedMatch(translations)
|
|
||||||
: { pretitle: undefined, title: filename, subtitle: undefined, language: undefined };
|
|
||||||
|
|
||||||
const hasTitle = title !== filename;
|
|
||||||
|
|
||||||
const attributesWithMeta = [
|
|
||||||
...attributes,
|
|
||||||
...(hasTitle
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: t("global.media.attributes.filename"),
|
|
||||||
icon: "material-symbols:unknown-document",
|
|
||||||
values: [{ name: filename }],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
title: t("global.media.attributes.filesize"),
|
|
||||||
icon: "material-symbols:hard-drive",
|
|
||||||
values: [{ name: formatFilesize(filesize) }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getFileTypeLabel = (): string => {
|
|
||||||
switch (mimeType) {
|
|
||||||
case "application/zip":
|
|
||||||
return t("global.previewTypes.zip");
|
|
||||||
|
|
||||||
case "application/pdf":
|
|
||||||
return t("global.previewTypes.pdf");
|
|
||||||
|
|
||||||
default:
|
|
||||||
return mimeType;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<GenericPreview
|
|
||||||
pretitle={pretitle}
|
|
||||||
title={title}
|
|
||||||
subtitle={subtitle}
|
|
||||||
lang={language}
|
|
||||||
thumbnail={thumbnail}
|
|
||||||
href={getLocalizedUrl(`/files/${id}`)}
|
|
||||||
attributes={attributesWithMeta}
|
|
||||||
icon={getFileIcon(mimeType)}
|
|
||||||
iconHoverLabel={getFileTypeLabel()}
|
|
||||||
smallTitle={!hasTitle}
|
|
||||||
/>
|
|
|
@ -1,58 +0,0 @@
|
||||||
---
|
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import { Collections } from "src/shared/payload/constants";
|
|
||||||
import type { EndpointFolder, EndpointFolderPreview } from "src/shared/payload/endpoint-types";
|
|
||||||
import type { Attribute } from "src/utils/attributes";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
folder: EndpointFolder | EndpointFolderPreview;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getLocalizedUrl, getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
|
|
||||||
const { folder } = Astro.props;
|
|
||||||
|
|
||||||
const { language, title } = getLocalizedMatch(folder.translations);
|
|
||||||
|
|
||||||
const attributes: Attribute[] = [];
|
|
||||||
|
|
||||||
if ("files" in folder) {
|
|
||||||
const { backlinks, files, sections } = folder;
|
|
||||||
|
|
||||||
const fileCount = files.length;
|
|
||||||
|
|
||||||
const subfolderCount =
|
|
||||||
sections.type === "single"
|
|
||||||
? sections.subfolders.length
|
|
||||||
: sections.sections.reduce((acc, section) => acc + section.subfolders.length, 0);
|
|
||||||
|
|
||||||
attributes.push({
|
|
||||||
icon: "material-symbols:box",
|
|
||||||
title: t("global.folders.attributes.content.label"),
|
|
||||||
values: [{ name: t("global.folders.attributes.content.value", { fileCount, subfolderCount }) }],
|
|
||||||
});
|
|
||||||
|
|
||||||
attributes.push({
|
|
||||||
icon: "material-symbols:keyboard-return",
|
|
||||||
title: t("global.folders.attributes.parent"),
|
|
||||||
values: backlinks.flatMap((link) => {
|
|
||||||
if (link.type !== Collections.Folders) return [];
|
|
||||||
const name = getLocalizedMatch(link.value.translations).title;
|
|
||||||
return { name };
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<GenericPreview
|
|
||||||
title={title}
|
|
||||||
lang={language}
|
|
||||||
href={getLocalizedUrl(`/folders/${folder.slug}`)}
|
|
||||||
attributes={attributes}
|
|
||||||
icon="material-symbols:folder-open"
|
|
||||||
iconHoverLabel={t("global.collections.folders", { count: 1 })}
|
|
||||||
smallTitle
|
|
||||||
/>
|
|
|
@ -1,4 +1,11 @@
|
||||||
---
|
---
|
||||||
|
import {
|
||||||
|
AttributeTypes,
|
||||||
|
type EndpointAttribute,
|
||||||
|
type EndpointImage,
|
||||||
|
type EndpointMediaThumbnail,
|
||||||
|
type EndpointScanImage,
|
||||||
|
} from "src/shared/payload/payload-sdk";
|
||||||
import Card from "components/Card.astro";
|
import Card from "components/Card.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import type { ComponentProps } from "astro/types";
|
import type { ComponentProps } from "astro/types";
|
||||||
|
@ -6,16 +13,9 @@ import { getI18n } from "src/i18n/i18n";
|
||||||
import InlineAttributes from "components/InlineAttributes.astro";
|
import InlineAttributes from "components/InlineAttributes.astro";
|
||||||
import { sizesToSrcset, sizesForGridLayout } from "src/utils/img";
|
import { sizesToSrcset, sizesForGridLayout } from "src/utils/img";
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
import type {
|
|
||||||
EndpointAttribute,
|
|
||||||
EndpointImagePreview,
|
|
||||||
EndpointPayloadImage,
|
|
||||||
EndpointScanImage,
|
|
||||||
} from "src/shared/payload/endpoint-types";
|
|
||||||
import { AttributeTypes } from "src/shared/payload/constants";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
thumbnail?: EndpointImagePreview | EndpointPayloadImage | EndpointScanImage | undefined;
|
thumbnail?: EndpointImage | EndpointMediaThumbnail | EndpointScanImage | undefined;
|
||||||
pretitle?: string | undefined;
|
pretitle?: string | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string | undefined;
|
subtitle?: string | undefined;
|
||||||
|
@ -113,18 +113,8 @@ for (const attribute of attributes) {
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
{
|
{
|
||||||
smallTitle ? (
|
smallTitle ? (
|
||||||
<p lang={lang}>
|
<p class="font-l" lang={lang}>
|
||||||
{pretitle && (
|
{title}
|
||||||
<span id="pretitle" class="font-s">
|
|
||||||
{pretitle}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span class="font-l">{title}</span>
|
|
||||||
{subtitle && (
|
|
||||||
<span id="subtitle" class="font-m">
|
|
||||||
{subtitle}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p lang={lang}>
|
<p lang={lang}>
|
||||||
|
@ -147,7 +137,9 @@ for (const attribute of attributes) {
|
||||||
clippedAttributes.length > 0 && (
|
clippedAttributes.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{subtitle && <hr />}
|
{subtitle && <hr />}
|
||||||
<InlineAttributes attributes={clippedAttributes} />
|
<div id="tags">
|
||||||
|
<InlineAttributes attributes={clippedAttributes} />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -175,21 +167,21 @@ for (const attribute of attributes) {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
&.rounded-top {
|
&.rounded-top {
|
||||||
border-top-left-radius: 14px;
|
border-top-left-radius: 1em;
|
||||||
border-top-right-radius: 14px;
|
border-top-right-radius: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #icon-container {
|
& > #icon-container {
|
||||||
&.thumbnail-alt {
|
&.thumbnail-alt {
|
||||||
margin: 6px;
|
margin: 0.4em;
|
||||||
margin-bottom: unset;
|
margin-bottom: unset;
|
||||||
aspect-ratio: 3/2;
|
aspect-ratio: 3/2;
|
||||||
background-color: var(--color-elevation-2);
|
background-color: var(--color-elevation-2);
|
||||||
color: var(--color-base-400);
|
color: var(--color-base-400);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
border-radius: 8px;
|
border-radius: 0.7em;
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
|
@ -199,18 +191,18 @@ for (const attribute of attributes) {
|
||||||
|
|
||||||
&:not(.thumbnail-alt) {
|
&:not(.thumbnail-alt) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 0.4em;
|
||||||
left: 6px;
|
left: 0.4em;
|
||||||
padding: 8px;
|
padding: 0.5em;
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
background-color: color-mix(in srgb, var(--color-elevation-2) 60%, transparent);
|
background-color: color-mix(in srgb, var(--color-elevation-2) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.rounded-top {
|
&.rounded-top {
|
||||||
border-radius: 8px;
|
border-radius: 0.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 0.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #footer {
|
& > #footer {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointImagePreview } from "src/shared/payload/endpoint-types";
|
import type { EndpointImage } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
image: EndpointImagePreview;
|
image: EndpointImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
|
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
@ -31,6 +31,6 @@ const { pretitle, title, subtitle, language } =
|
||||||
href={getLocalizedUrl(`/images/${id}`)}
|
href={getLocalizedUrl(`/images/${id}`)}
|
||||||
attributes={attributes}
|
attributes={attributes}
|
||||||
icon="material-symbols:imagesmode"
|
icon="material-symbols:imagesmode"
|
||||||
iconHoverLabel={t("global.collections.images", { count: 1 })}
|
iconHoverLabel={t("global.previewTypes.image")}
|
||||||
smallTitle={title === filename}
|
smallTitle={title === filename}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointPagePreview } from "src/shared/payload/endpoint-types";
|
import type { EndpointPage } from "src/shared/payload/payload-sdk";
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: EndpointPagePreview;
|
page: EndpointPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLocalizedMatch, getLocalizedUrl, t, formatDate } = await getI18n(
|
const { getLocalizedMatch, getLocalizedUrl, t, formatDate } = await getI18n(
|
||||||
|
@ -39,5 +39,5 @@ const metaAttributes: Attribute[] = [
|
||||||
href={getLocalizedUrl(`/pages/${slug}`)}
|
href={getLocalizedUrl(`/pages/${slug}`)}
|
||||||
attributes={[...attributes, ...metaAttributes]}
|
attributes={[...attributes, ...metaAttributes]}
|
||||||
icon="material-symbols:docs"
|
icon="material-symbols:docs"
|
||||||
iconHoverLabel={t("global.collections.pages", { count: 1 })}
|
iconHoverLabel={t("global.previewTypes.page")}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import type { EndpointRecorder } from "src/shared/payload/endpoint-types";
|
|
||||||
import type { Attribute } from "src/utils/attributes";
|
|
||||||
import { formatLocale } from "src/utils/format";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
recorder: EndpointRecorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
|
|
||||||
const {
|
|
||||||
recorder: { id, languages, username, avatar },
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const attributes: Attribute[] = [
|
|
||||||
{
|
|
||||||
icon: "material-symbols:translate",
|
|
||||||
title: t("collectibles.languages"),
|
|
||||||
values: languages.map((id) => ({ name: formatLocale(id) })),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<GenericPreview
|
|
||||||
title={username}
|
|
||||||
thumbnail={avatar}
|
|
||||||
href={getLocalizedUrl(`/recorders/${id}`)}
|
|
||||||
attributes={attributes}
|
|
||||||
icon="material-symbols:person"
|
|
||||||
iconHoverLabel={t("global.collections.recorders", { count: 1 })}
|
|
||||||
/>
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointVideoPreview } from "src/shared/payload/endpoint-types";
|
import type { EndpointVideo } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
video: EndpointVideoPreview;
|
video: EndpointVideo;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLocalizedMatch, getLocalizedUrl, t, formatDuration } = await getI18n(
|
const { getLocalizedMatch, getLocalizedUrl, t, formatDuration } = await getI18n(
|
||||||
|
@ -41,6 +41,6 @@ const attributesWithMeta = [
|
||||||
href={getLocalizedUrl(`/videos/${id}`)}
|
href={getLocalizedUrl(`/videos/${id}`)}
|
||||||
attributes={attributesWithMeta}
|
attributes={attributesWithMeta}
|
||||||
icon="material-symbols:smart-display"
|
icon="material-symbols:smart-display"
|
||||||
iconHoverLabel={t("global.collections.videos", { count: 1 })}
|
iconHoverLabel={t("global.previewTypes.video")}
|
||||||
smallTitle={title === filename}
|
smallTitle={title === filename}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
---
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
relation: EndpointRelation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { relation } = Astro.props;
|
|
||||||
const { formatEndpointRelation } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
|
|
||||||
const {
|
|
||||||
href,
|
|
||||||
typeLabel,
|
|
||||||
label,
|
|
||||||
target = undefined,
|
|
||||||
rel = undefined,
|
|
||||||
lang,
|
|
||||||
} = formatEndpointRelation(relation);
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<a href={href} target={target} rel={rel}>
|
|
||||||
<p>
|
|
||||||
<span id="type" class="font-xs">{typeLabel}</span><span id="label" lang={lang}>{label}</span>
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
margin: -4px -10px;
|
|
||||||
|
|
||||||
& > p {
|
|
||||||
display: flex;
|
|
||||||
place-items: start;
|
|
||||||
gap: 0.1em 0.3em;
|
|
||||||
|
|
||||||
& > #label {
|
|
||||||
text-decoration: underline dotted 0.1em;
|
|
||||||
text-decoration-color: transparent;
|
|
||||||
|
|
||||||
transition-duration: 150ms;
|
|
||||||
transition-property: text-decoration-color, color;
|
|
||||||
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > #type {
|
|
||||||
background-color: var(--color-base-300);
|
|
||||||
border-radius: 9999px;
|
|
||||||
padding: 0.3em 0.6em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: -0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover > p > #label,
|
|
||||||
&:focus-visible > p > #label {
|
|
||||||
color: var(--color-base-750);
|
|
||||||
text-decoration-color: var(--color-base-650);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline-width: 1.5 px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active > p > #label {
|
|
||||||
color: var(--color-base-650);
|
|
||||||
text-decoration-color: var(--color-base-550);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,140 +0,0 @@
|
||||||
---
|
|
||||||
import ChronologyEventPreview from "components/Previews/ChronologyEventPreview.astro";
|
|
||||||
import CollectiblePreview from "components/Previews/CollectiblePreview.astro";
|
|
||||||
import FolderPreview from "components/Previews/FolderPreview.astro";
|
|
||||||
import { Collections } from "src/shared/payload/constants";
|
|
||||||
import type {
|
|
||||||
EndpointChronologyEvent,
|
|
||||||
EndpointCollectiblePreview,
|
|
||||||
EndpointFolderPreview,
|
|
||||||
EndpointRelation,
|
|
||||||
} from "src/shared/payload/endpoint-types";
|
|
||||||
import ReturnToParentCard from "./AppLayout/components/Topbar/components/ReturnToParentCard.astro";
|
|
||||||
import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import AppLayout from "./AppLayout/AppLayout.astro";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
parentPage: EndpointRelation;
|
|
||||||
backlinks: EndpointRelation[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { backlinks, parentPage } = Astro.props;
|
|
||||||
const { formatEndpointRelation, t } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
const { label } = formatEndpointRelation(parentPage);
|
|
||||||
|
|
||||||
const collectibles: EndpointCollectiblePreview[] = [];
|
|
||||||
const folders: EndpointFolderPreview[] = [];
|
|
||||||
const events: EndpointChronologyEvent[] = [];
|
|
||||||
|
|
||||||
backlinks.forEach((relation) => {
|
|
||||||
switch (relation.type) {
|
|
||||||
case Collections.Collectibles:
|
|
||||||
collectibles.push(relation.value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Collections.Folders:
|
|
||||||
folders.push(relation.value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Collections.ChronologyEvents:
|
|
||||||
events.push(relation.value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
---
|
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
||||||
|
|
||||||
<AppLayout class="app">
|
|
||||||
<AppLayoutTitle title={t("global.relationPage.title")} />
|
|
||||||
|
|
||||||
<ReturnToParentCard parent={parentPage} />
|
|
||||||
|
|
||||||
{
|
|
||||||
collectibles.length > 0 && (
|
|
||||||
<section>
|
|
||||||
<h2 class="font-3xl font-serif">
|
|
||||||
{t("global.collections.collectibles", { count: collectibles.length })}
|
|
||||||
</h2>
|
|
||||||
<p>{t("global.relationPage.collectibles", { title: label, count: folders.length })}</p>
|
|
||||||
<div class="grid">
|
|
||||||
{collectibles.map((collectible) => (
|
|
||||||
<CollectiblePreview collectible={collectible} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
folders.length > 0 && (
|
|
||||||
<section>
|
|
||||||
<h2 class="font-3xl font-serif">
|
|
||||||
{t("global.collections.folders", { count: folders.length })}
|
|
||||||
</h2>
|
|
||||||
<p>{t("global.relationPage.folders", { title: label, count: folders.length })}</p>
|
|
||||||
<div class="grid">
|
|
||||||
{folders.map((folder) => (
|
|
||||||
<FolderPreview folder={folder} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
events.length > 0 && (
|
|
||||||
<section>
|
|
||||||
<h2 class="font-3xl font-serif">{t("global.collections.chronologyEvents")}</h2>
|
|
||||||
<p>{t("global.relationPage.timelineEvents", { title: label, count: folders.length })}</p>
|
|
||||||
<div class="grid">
|
|
||||||
{events.map(({ date, events }) =>
|
|
||||||
events.map((event) => <ChronologyEventPreview date={date} event={event} />)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</AppLayout>
|
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: clamp(6px, 2vmin, 16px);
|
|
||||||
align-items: start;
|
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
row-gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 24rem) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
margin-block: 3em;
|
|
||||||
|
|
||||||
& > h2 {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > p {
|
|
||||||
margin-bottom: 3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
|
import type { RichTextContent } from "src/shared/payload/payload-sdk";
|
||||||
import RTNode from "./components/RTNode.astro";
|
import RTNode from "./components/RTNode.astro";
|
||||||
import RTProse from "./components/RTProse.astro";
|
import RTProse from "./components/RTProse.astro";
|
||||||
import { type RichTextContext } from "src/utils/richText";
|
import { type RichTextContext } from "src/utils/richText";
|
||||||
import ConditionalWrapper from "components/ConditionalWrapper.astro";
|
import ConditionalWrapper from "components/ConditionalWrapper.astro";
|
||||||
import type { RichTextContent } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: RichTextContent;
|
content: RichTextContent;
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import RTSection from "./components/RTSection.astro";
|
import RTSection from "./components/RTSection.astro";
|
||||||
import RTTranscript from "./components/RTTranscript.astro";
|
import RTTranscript from "./components/RTTranscript.astro";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
|
||||||
import RTBreak from "./components/RTBreak.astro";
|
|
||||||
import {
|
import {
|
||||||
type RichTextBlockNode,
|
isBlockNodeBreakBlock,
|
||||||
isBlockNodeSectionBlock,
|
isBlockNodeSectionBlock,
|
||||||
isBlockNodeTranscriptBlock,
|
isBlockNodeTranscriptBlock,
|
||||||
isBlockNodeBreakBlock,
|
type RichTextBlockNode,
|
||||||
} from "src/shared/payload/rich-text";
|
} from "src/shared/payload/payload-sdk";
|
||||||
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
|
import RTBreak from "./components/RTBreak.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextBlockNode;
|
node: RichTextBlockNode;
|
||||||
|
@ -29,6 +29,9 @@ const { node, context } = Astro.props;
|
||||||
) : isBlockNodeBreakBlock(node) ? (
|
) : isBlockNodeBreakBlock(node) ? (
|
||||||
<RTBreak node={node} context={context} />
|
<RTBreak node={node} context={context} />
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown block type: ${node.fields.blockType}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown block type: ${node.fields.blockType}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
|
import { BreakBlockType, type RichTextBreakBlock } from "src/shared/payload/payload-sdk";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
import { BreakBlockType } from "src/shared/payload/constants";
|
|
||||||
import type { RichTextBreakBlock } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextBreakBlock;
|
node: RichTextBreakBlock;
|
||||||
|
@ -30,7 +29,10 @@ const { node } = Astro.props;
|
||||||
) : node.fields.type === BreakBlockType.solidLine ? (
|
) : node.fields.type === BreakBlockType.solidLine ? (
|
||||||
<hr id={node.anchorHash} class="solid" />
|
<hr id={node.anchorHash} class="solid" />
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown break block type: ${node.fields.type}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown break block type: ${node.fields.type}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import HeaderTitle from "components/HeaderTitle.astro";
|
import HeaderTitle from "components/HeaderTitle.astro";
|
||||||
import RichText from "components/RichText/RichText.astro";
|
import RichText from "components/RichText/RichText.astro";
|
||||||
import type { RichTextSectionBlock } from "src/shared/payload/rich-text";
|
import type { RichTextSectionBlock } from "src/shared/payload/payload-sdk";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
|
import type { RichTextTranscriptBlock } from "src/shared/payload/payload-sdk";
|
||||||
import Block from "components/Blocks/Block.astro";
|
import Block from "components/Blocks/Block.astro";
|
||||||
import type { RichTextTranscriptBlock } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextTranscriptBlock;
|
node: RichTextTranscriptBlock;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import type { RichTextLinebreakNode } from "src/shared/payload/rich-text";
|
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
|
import type { RichTextLinebreakNode } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextLinebreakNode;
|
node: RichTextLinebreakNode;
|
||||||
|
|
|
@ -3,12 +3,12 @@ import type { RichTextContext } from "src/utils/richText";
|
||||||
import RTNode from "../RTNode.astro";
|
import RTNode from "../RTNode.astro";
|
||||||
import RTCustomLink from "./components/RTCustomLink.astro";
|
import RTCustomLink from "./components/RTCustomLink.astro";
|
||||||
import RTInternalLink from "./components/RTInternalLink.astro";
|
import RTInternalLink from "./components/RTInternalLink.astro";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
|
||||||
import {
|
import {
|
||||||
isLinkNodeCustomLinkNode,
|
isLinkNodeCustomLinkNode,
|
||||||
isLinkNodeInternalLinkNode,
|
isLinkNodeInternalLinkNode,
|
||||||
type RichTextLinkNode,
|
type RichTextLinkNode,
|
||||||
} from "src/shared/payload/rich-text";
|
} from "src/shared/payload/payload-sdk";
|
||||||
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextLinkNode;
|
node: RichTextLinkNode;
|
||||||
|
@ -34,6 +34,9 @@ const { node, context } = Astro.props;
|
||||||
))}
|
))}
|
||||||
</RTInternalLink>
|
</RTInternalLink>
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown link type: ${node.fields.linkType}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown link type: ${node.fields.linkType}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { Collections } from "src/shared/payload/constants";
|
import { Collections } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
doc: {
|
doc: {
|
||||||
|
@ -30,6 +30,9 @@ const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown internal link: ${doc.relationTo}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown internal link: ${doc.relationTo}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import RTBasicListItem from "./components/RTBasicListItem.astro";
|
import RTBasicListItem from "./components/RTBasicListItem.astro";
|
||||||
import RTCheckListItem from "./components/RTCheckListItem.astro";
|
import RTCheckListItem from "./components/RTCheckListItem.astro";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
|
||||||
import {
|
import {
|
||||||
type RichTextListNode,
|
|
||||||
isListNodeNumberListNode,
|
|
||||||
isListNodeBulletListNode,
|
isListNodeBulletListNode,
|
||||||
isListNodeCheckListNode,
|
isListNodeCheckListNode,
|
||||||
} from "src/shared/payload/rich-text";
|
isListNodeNumberListNode,
|
||||||
|
type RichTextListNode,
|
||||||
|
} from "src/shared/payload/payload-sdk";
|
||||||
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextListNode;
|
node: RichTextListNode;
|
||||||
|
@ -40,6 +40,9 @@ const { node, context } = Astro.props;
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown list link: ${node.listType}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown list link: ${node.listType}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,21 +5,21 @@ import RTText from "./RTText/RTText.astro";
|
||||||
import RTLink from "./RTLink/RTLink.astro";
|
import RTLink from "./RTLink/RTLink.astro";
|
||||||
import RTBlock from "./RTBlock/RTBlock.astro";
|
import RTBlock from "./RTBlock/RTBlock.astro";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
|
import {
|
||||||
|
isNodeBlockNode,
|
||||||
|
isNodeLinebreakNode,
|
||||||
|
isNodeLinkNode,
|
||||||
|
isNodeListNode,
|
||||||
|
isNodeParagraphNode,
|
||||||
|
isNodeTabNode,
|
||||||
|
isNodeTextNode,
|
||||||
|
isNodeUploadNode,
|
||||||
|
type RichTextNode,
|
||||||
|
} from "src/shared/payload/payload-sdk";
|
||||||
import RTTab from "./RTTab.astro";
|
import RTTab from "./RTTab.astro";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
import RTLinebreak from "./RTLinebreak.astro";
|
import RTLinebreak from "./RTLinebreak.astro";
|
||||||
import RTUpload from "./RTUpload/RTUpload.astro";
|
import RTUpload from "./RTUpload/RTUpload.astro";
|
||||||
import {
|
|
||||||
type RichTextNode,
|
|
||||||
isNodeParagraphNode,
|
|
||||||
isNodeListNode,
|
|
||||||
isNodeTextNode,
|
|
||||||
isNodeLinebreakNode,
|
|
||||||
isNodeLinkNode,
|
|
||||||
isNodeBlockNode,
|
|
||||||
isNodeTabNode,
|
|
||||||
isNodeUploadNode,
|
|
||||||
} from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextNode;
|
node: RichTextNode;
|
||||||
|
@ -49,6 +49,9 @@ const { node, context } = Astro.props;
|
||||||
) : isNodeUploadNode(node) ? (
|
) : isNodeUploadNode(node) ? (
|
||||||
<RTUpload node={node} context={context} />
|
<RTUpload node={node} context={context} />
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown node type: ${node.type}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown node type: ${node.type}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import RTNode from "./RTNode.astro";
|
import RTNode from "./RTNode.astro";
|
||||||
import type { RichTextParagraphNode } from "src/shared/payload/rich-text";
|
import type { RichTextParagraphNode } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextParagraphNode;
|
node: RichTextParagraphNode;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import RTSubscript from "./components/RTSubscript.astro";
|
||||||
import RTSuperscript from "./components/RTSuperscript.astro";
|
import RTSuperscript from "./components/RTSuperscript.astro";
|
||||||
import RTInlineCode from "./components/RTInlineCode.astro";
|
import RTInlineCode from "./components/RTInlineCode.astro";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import type { RichTextTextNode } from "src/shared/payload/rich-text";
|
import type { RichTextTextNode } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextTextNode;
|
node: RichTextTextNode;
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
---
|
---
|
||||||
|
import {
|
||||||
|
isUploadNodeAudioNode,
|
||||||
|
isUploadNodeImageNode,
|
||||||
|
isUploadNodeVideoNode,
|
||||||
|
type RichTextUploadNode,
|
||||||
|
} from "src/shared/payload/payload-sdk";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import RTImage from "./components/RTImage.astro";
|
import RTImage from "./components/RTImage.astro";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
import RTAudio from "./components/RTAudio.astro";
|
import RTAudio from "./components/RTAudio.astro";
|
||||||
import RTVideo from "./components/RTVideo.astro";
|
import RTVideo from "./components/RTVideo.astro";
|
||||||
import {
|
|
||||||
type RichTextUploadNode,
|
|
||||||
isUploadNodeImageNode,
|
|
||||||
isUploadNodeAudioNode,
|
|
||||||
isUploadNodeVideoNode,
|
|
||||||
} from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextUploadNode;
|
node: RichTextUploadNode;
|
||||||
|
@ -29,6 +29,9 @@ const { node, context } = Astro.props;
|
||||||
) : isUploadNodeVideoNode(node) ? (
|
) : isUploadNodeVideoNode(node) ? (
|
||||||
<RTVideo node={node} context={context} />
|
<RTVideo node={node} context={context} />
|
||||||
) : (
|
) : (
|
||||||
<ErrorMessage title={`Unknown upload collection: ${node.relationTo}`} />
|
<ErrorMessage
|
||||||
|
title={`Unknown upload collection: ${node.relationTo}`}
|
||||||
|
description="Please contact website technical administrator."
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
|
import { type RichTextUploadAudioNode } from "src/shared/payload/payload-sdk";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import AudioPlayer from "components/AudioPlayer.astro";
|
import AudioPlayer from "components/AudioPlayer.astro";
|
||||||
import HeaderTitle from "components/HeaderTitle.astro";
|
import HeaderTitle from "components/HeaderTitle.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
|
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
|
||||||
import type { RichTextUploadAudioNode } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextUploadAudioNode;
|
node: RichTextUploadAudioNode;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
---
|
---
|
||||||
|
import { type RichTextUploadImageNode } from "src/shared/payload/payload-sdk";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
|
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import HeaderTitle from "components/HeaderTitle.astro";
|
import HeaderTitle from "components/HeaderTitle.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { sizesToSrcset } from "src/utils/img";
|
import { sizesToSrcset } from "src/utils/img";
|
||||||
import type { RichTextUploadImageNode } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextUploadImageNode;
|
node: RichTextUploadImageNode;
|
||||||
|
@ -46,6 +47,7 @@ const mediaPage = getLocalizedUrl(`/images/${id}`);
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<OpenMediaPageButton url={mediaPage} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
@ -53,13 +55,11 @@ const mediaPage = getLocalizedUrl(`/images/${id}`);
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
margin-block: 4em;
|
margin-block: 4em;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 5px 20px -10px var(--color-shadow-0);
|
box-shadow: 0 5px 20px -10px var(--color-shadow-0);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
@ -71,6 +71,7 @@ const mediaPage = getLocalizedUrl(`/images/${id}`);
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
scale: 102%;
|
scale: 102%;
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
|
import { type RichTextUploadVideoNode } from "src/shared/payload/payload-sdk";
|
||||||
import type { RichTextContext } from "src/utils/richText";
|
import type { RichTextContext } from "src/utils/richText";
|
||||||
import VideoPlayer from "components/VideoPlayer.astro";
|
import VideoPlayer from "components/VideoPlayer.astro";
|
||||||
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
|
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import HeaderTitle from "components/HeaderTitle.astro";
|
import HeaderTitle from "components/HeaderTitle.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import type { RichTextUploadVideoNode } from "src/shared/payload/rich-text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: RichTextUploadVideoNode;
|
node: RichTextUploadVideoNode;
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
---
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
import type { EndpointSource } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
source: EndpointSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { source } = Astro.props;
|
||||||
|
const { formatEndpointSource } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
|
const {
|
||||||
|
href,
|
||||||
|
typeLabel,
|
||||||
|
label,
|
||||||
|
target = undefined,
|
||||||
|
rel = undefined,
|
||||||
|
lang,
|
||||||
|
} = formatEndpointSource(source);
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
|
<a href={href} target={target} rel={rel}>
|
||||||
|
<div class="font-xs">{typeLabel}</div><p lang={lang}>{label}</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
place-items: center;
|
||||||
|
gap: 0.1em 0.3em;
|
||||||
|
|
||||||
|
& > p {
|
||||||
|
text-decoration: underline dotted 0.1em;
|
||||||
|
text-decoration-color: transparent;
|
||||||
|
|
||||||
|
transition-duration: 150ms;
|
||||||
|
transition-property: text-decoration-color, color;
|
||||||
|
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin: -4px -10px;
|
||||||
|
|
||||||
|
&:hover > p,
|
||||||
|
&:focus-visible > p {
|
||||||
|
color: var(--color-base-750);
|
||||||
|
text-decoration-color: var(--color-base-650);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-width: 1.5 px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active > p {
|
||||||
|
color: var(--color-base-650);
|
||||||
|
text-decoration-color: var(--color-base-550);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
background-color: var(--color-base-300);
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: -0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
|
import type { TableOfContentEntry } from "src/shared/payload/payload-sdk";
|
||||||
import TableOfContentItem from "./components/TableOfContentItem.astro";
|
import TableOfContentItem from "./components/TableOfContentItem.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import TitleIcon from "components/TitleIcon.astro";
|
import TitleIcon from "components/TitleIcon.astro";
|
||||||
import type { TableOfContentEntry } from "src/shared/payload/endpoint-types";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
toc: TableOfContentEntry[];
|
toc: TableOfContentEntry[];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { TableOfContentEntry } from "src/shared/payload/endpoint-types";
|
import type { TableOfContentEntry } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entry: TableOfContentEntry;
|
entry: TableOfContentEntry;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
import type { EndpointVideoPreview } from "src/shared/payload/endpoint-types";
|
import type { EndpointVideo } from "src/shared/payload/payload-sdk";
|
||||||
import { formatLocale } from "src/utils/format";
|
import { formatLocale } from "src/utils/format";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
video: EndpointVideoPreview;
|
video: EndpointVideo;
|
||||||
class?: string | undefined;
|
class?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
declare namespace App {
|
declare namespace App {
|
||||||
interface Locals {
|
interface Locals {
|
||||||
currentLocale: string;
|
currentLocale: string;
|
||||||
pageCaching: boolean;
|
currentTheme: "dark" | "auto" | "light";
|
||||||
|
currentCurrency: string;
|
||||||
|
notFound: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
123
src/i18n/i18n.ts
123
src/i18n/i18n.ts
|
@ -1,16 +1,12 @@
|
||||||
import type { WordingKey } from "src/i18n/wordings-keys";
|
import type { WordingKey } from "src/i18n/wordings-keys";
|
||||||
import { contextCache } from "src/services";
|
import type { ChronologyEvent, EndpointSource } from "src/shared/payload/payload-sdk";
|
||||||
import {
|
import { cache } from "src/utils/payload";
|
||||||
capitalize,
|
import { capitalize, formatInlineTitle } from "src/utils/format";
|
||||||
formatInlineTitle,
|
|
||||||
formatRichTextToString,
|
|
||||||
formatTimelineDateToId,
|
|
||||||
} from "src/utils/format";
|
|
||||||
import type { EndpointChronologyEvent, EndpointRelation } from "src/shared/payload/endpoint-types";
|
|
||||||
import { Collections } from "src/shared/payload/constants";
|
|
||||||
|
|
||||||
export const defaultLocale = "en";
|
export const defaultLocale = "en";
|
||||||
|
|
||||||
|
export type I18n = Awaited<ReturnType<typeof getI18n>>;
|
||||||
|
|
||||||
export const getI18n = async (locale: string) => {
|
export const getI18n = async (locale: string) => {
|
||||||
const formatWithValues = (
|
const formatWithValues = (
|
||||||
templateName: string,
|
templateName: string,
|
||||||
|
@ -119,7 +115,7 @@ export const getI18n = async (locale: string) => {
|
||||||
options[0]!; // We will consider that there will always be at least one option.
|
options[0]!; // We will consider that there will always be at least one option.
|
||||||
|
|
||||||
const t = (key: WordingKey, values: Record<string, any> = {}): string => {
|
const t = (key: WordingKey, values: Record<string, any> = {}): string => {
|
||||||
const wording = contextCache.wordings.find(({ name }) => name === key);
|
const wording = cache.wordings.find(({ name }) => name === key);
|
||||||
const fallbackString = `«${key}»`;
|
const fallbackString = `«${key}»`;
|
||||||
|
|
||||||
if (!wording) {
|
if (!wording) {
|
||||||
|
@ -183,7 +179,7 @@ export const getI18n = async (locale: string) => {
|
||||||
return number.toLocaleString(locale, options);
|
return number.toLocaleString(locale, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTimelineDate = ({ year, month, day }: EndpointChronologyEvent["date"]): string => {
|
const formatTimelineDate = ({ year, month, day }: ChronologyEvent["date"]): string => {
|
||||||
const date = new Date(0);
|
const date = new Date(0);
|
||||||
date.setFullYear(year);
|
date.setFullYear(year);
|
||||||
if (month) date.setMonth(month - 1);
|
if (month) date.setMonth(month - 1);
|
||||||
|
@ -242,8 +238,8 @@ export const getI18n = async (locale: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEndpointRelation = (
|
const formatEndpointSource = (
|
||||||
relation: EndpointRelation
|
source: EndpointSource
|
||||||
): {
|
): {
|
||||||
href: string;
|
href: string;
|
||||||
typeLabel: string;
|
typeLabel: string;
|
||||||
|
@ -252,107 +248,98 @@ export const getI18n = async (locale: string) => {
|
||||||
target?: string;
|
target?: string;
|
||||||
rel?: string;
|
rel?: string;
|
||||||
} => {
|
} => {
|
||||||
switch (relation.type) {
|
switch (source.type) {
|
||||||
case "url":
|
case "url": {
|
||||||
return {
|
return {
|
||||||
href: relation.value.url,
|
href: source.url,
|
||||||
typeLabel: t("global.sources.typeLabel.url"),
|
typeLabel: t("global.sources.typeLabel.url"),
|
||||||
label: relation.value.label,
|
label: source.label,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noopener noreferrer",
|
rel: "noopener noreferrer",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case Collections.Collectibles: {
|
case "collectible": {
|
||||||
const getRangeLabel = () => {
|
const rangeLabel = (() => {
|
||||||
switch (relation.range?.type) {
|
switch (source.range?.type) {
|
||||||
case "timestamp":
|
case "timestamp":
|
||||||
return t("global.sources.typeLabel.collectible.range.timestamp", {
|
return t("global.sources.typeLabel.collectible.range.timestamp", {
|
||||||
page: relation.range.timestamp,
|
page: source.range.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
case "page":
|
case "page":
|
||||||
return t("global.sources.typeLabel.collectible.range.page", {
|
return t("global.sources.typeLabel.collectible.range.page", {
|
||||||
page: relation.range.page,
|
page: source.range.page,
|
||||||
});
|
});
|
||||||
|
|
||||||
case "custom":
|
case "custom":
|
||||||
return t("global.sources.typeLabel.collectible.range.custom", {
|
return t("global.sources.typeLabel.collectible.range.custom", {
|
||||||
note: getLocalizedMatch(relation.range.translations).note,
|
note: getLocalizedMatch(source.range.translations).note,
|
||||||
});
|
});
|
||||||
|
|
||||||
case undefined:
|
case undefined:
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
const suffix =
|
const translation = getLocalizedMatch(source.collectible.translations);
|
||||||
relation.subpage === "scans"
|
|
||||||
? "/scans"
|
|
||||||
: relation.subpage === "gallery"
|
|
||||||
? "/gallery"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const translation = getLocalizedMatch(relation.value.translations);
|
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/collectibles/${relation.value.slug}${suffix}`),
|
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}`),
|
||||||
typeLabel: t("global.sources.typeLabel.collectible"),
|
typeLabel: t("global.sources.typeLabel.collectible"),
|
||||||
label: formatInlineTitle(translation) + getRangeLabel(),
|
label: formatInlineTitle(translation) + rangeLabel,
|
||||||
lang: translation.language,
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case Collections.Pages: {
|
case "page": {
|
||||||
const translation = getLocalizedMatch(relation.value.translations);
|
const translation = getLocalizedMatch(source.page.translations);
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/pages/${relation.value.slug}`),
|
href: getLocalizedUrl(`/pages/${source.page.slug}`),
|
||||||
typeLabel: t("global.sources.typeLabel.page"),
|
typeLabel: t("global.sources.typeLabel.page"),
|
||||||
label: formatInlineTitle(translation),
|
label: formatInlineTitle(translation),
|
||||||
lang: translation.language,
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case Collections.Folders: {
|
case "folder": {
|
||||||
const translation = getLocalizedMatch(relation.value.translations);
|
const translation = getLocalizedMatch(source.folder.translations);
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/folders/${relation.value.slug}`),
|
href: getLocalizedUrl(`/folders/${source.folder.slug}`),
|
||||||
typeLabel: t("global.sources.typeLabel.folder"),
|
typeLabel: t("global.sources.typeLabel.folder"),
|
||||||
label: formatInlineTitle(translation),
|
label: getLocalizedMatch(source.folder.translations).name,
|
||||||
lang: translation.language,
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case Collections.ChronologyEvents: {
|
case "scans": {
|
||||||
if (!relation.value.events[0]) break;
|
const translation = getLocalizedMatch(source.collectible.translations);
|
||||||
|
|
||||||
const translation = getLocalizedMatch(relation.value.events[0].translations);
|
|
||||||
let label =
|
|
||||||
translation.title ??
|
|
||||||
(translation.description && formatRichTextToString(translation.description));
|
|
||||||
if (!label) break;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/timeline#${formatTimelineDateToId(relation.value.date)}`),
|
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/scans`),
|
||||||
typeLabel: t("global.sources.typeLabel.timeline"),
|
typeLabel: t("global.sources.typeLabel.scans"),
|
||||||
label,
|
label: formatInlineTitle(getLocalizedMatch(source.collectible.translations)),
|
||||||
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Handle other types of relations */
|
case "gallery": {
|
||||||
case Collections.Audios:
|
const translation = getLocalizedMatch(source.collectible.translations);
|
||||||
case Collections.Files:
|
return {
|
||||||
case Collections.Images:
|
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/gallery`),
|
||||||
case Collections.Recorders:
|
typeLabel: t("global.sources.typeLabel.gallery"),
|
||||||
case Collections.Tags:
|
label: formatInlineTitle(getLocalizedMatch(source.collectible.translations)),
|
||||||
case Collections.Videos:
|
lang: translation.language,
|
||||||
default:
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
default: {
|
||||||
href: "/404",
|
return {
|
||||||
label: `Invalid type ${relation["type"]}`,
|
href: "/404",
|
||||||
typeLabel: "Error",
|
label: `Invalid type ${source["type"]}`,
|
||||||
};
|
typeLabel: "Error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -368,7 +355,7 @@ export const getI18n = async (locale: string) => {
|
||||||
formatMillimeters,
|
formatMillimeters,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
formatTimelineDate,
|
formatTimelineDate,
|
||||||
formatEndpointRelation,
|
formatEndpointSource,
|
||||||
formatScanIndexShort,
|
formatScanIndexShort,
|
||||||
formatFilesize,
|
formatFilesize,
|
||||||
};
|
};
|
||||||
|
|
|
@ -85,7 +85,7 @@ export type WordingKey =
|
||||||
| "global.loading"
|
| "global.loading"
|
||||||
| "pages.tableOfContent.sceneBreak"
|
| "pages.tableOfContent.sceneBreak"
|
||||||
| "pages.tableOfContent.break"
|
| "pages.tableOfContent.break"
|
||||||
| "pages.credits.availableLanguages"
|
| "global.languageOverride.availableLanguages"
|
||||||
| "timeline.title"
|
| "timeline.title"
|
||||||
| "timeline.description"
|
| "timeline.description"
|
||||||
| "timeline.eras.cataclysm"
|
| "timeline.eras.cataclysm"
|
||||||
|
@ -147,43 +147,4 @@ export type WordingKey =
|
||||||
| "collectibles.nature"
|
| "collectibles.nature"
|
||||||
| "collectibles.languages"
|
| "collectibles.languages"
|
||||||
| "collectibles.nature.physical"
|
| "collectibles.nature.physical"
|
||||||
| "collectibles.nature.digital"
|
| "collectibles.nature.digital";
|
||||||
| "global.previewTypes.zip"
|
|
||||||
| "global.previewTypes.pdf"
|
|
||||||
| "collectibles.files"
|
|
||||||
| "pages.credits.translationLabel"
|
|
||||||
| "pages.credits.currentLocale"
|
|
||||||
| "global.media.attributes.resolution"
|
|
||||||
| "search.searchBar.placeholder"
|
|
||||||
| "global.collections.collectibles"
|
|
||||||
| "global.collections.pages"
|
|
||||||
| "global.collections.folders"
|
|
||||||
| "global.collections.audios"
|
|
||||||
| "global.collections.videos"
|
|
||||||
| "global.collections.images"
|
|
||||||
| "global.collections.files"
|
|
||||||
| "global.collections.recorders"
|
|
||||||
| "global.collections.chronologyEvents"
|
|
||||||
| "global.collections.all"
|
|
||||||
| "search.resultCount"
|
|
||||||
| "search.searchBar.submitButton.tooltip"
|
|
||||||
| "search.noResult.title"
|
|
||||||
| "search.noResult.message"
|
|
||||||
| "search.collectionFilter.tooltip"
|
|
||||||
| "paginator.goFirstPageButton"
|
|
||||||
| "paginator.goPreviousPageButton"
|
|
||||||
| "paginator.goNextPageButton"
|
|
||||||
| "paginator.goLastPageButton"
|
|
||||||
| "global.folders.attributes.content.label"
|
|
||||||
| "global.folders.attributes.content.value"
|
|
||||||
| "global.folders.attributes.parent"
|
|
||||||
| "global.sources.typeLabel.timeline"
|
|
||||||
| "global.subpageCard.message"
|
|
||||||
| "global.subpageCard.returnButton"
|
|
||||||
| "header.nav.parentPages.tooltip.viewAll"
|
|
||||||
| "global.relationPage.folders"
|
|
||||||
| "global.relationPage.collectibles"
|
|
||||||
| "global.relationPage.timelineEvents"
|
|
||||||
| "global.relationPage.title"
|
|
||||||
| "pages.credits.sources";
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" version="1.1" id="svg1" xml:space="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs id="defs1" />
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M 6,22 C 5.45,22 4.979,21.804 4.587,21.412 4.195,21.02 3.9993333,20.549333 4,20 V 4 C 4,3.45 4.196,2.979 4.588,2.587 4.98,2.195 5.4506667,1.9993333 6,2 h 8 l 6,6 v 12 c 0,0.55 -0.196,1.021 -0.588,1.413 C 19.02,21.805 18.549333,22.000667 18,22 Z M 13,9 h 5 L 13,4 Z m -5.8880933,9.669723 h 9.5724213 l -2.991381,-3.98851 -2.393106,3.190808 -1.7948288,-2.393105 z M 9.1061613,13.08581 c 0.332376,0 0.6150281,-0.116465 0.8479576,-0.349393 0.2329281,-0.23293 0.3491271,-0.515317 0.3485951,-0.84716 0,-0.332376 -0.116464,-0.615029 -0.3493937,-0.847957 -0.2329286,-0.232929 -0.5153151,-0.349128 -0.847159,-0.348596 -0.3323761,0 -0.6150282,0.116464 -0.8479571,0.349393 -0.232929,0.23293 -0.3491278,0.515316 -0.3485957,0.84716 0,0.332376 0.1164644,0.615028 0.3493934,0.847957 0.2329289,0.232929 0.5153154,0.349128 0.8471594,0.348596 z"
|
|
||||||
id="path1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" version="1.1" id="svg1" xml:space="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs id="defs1" />
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M 6,22 C 5.45,22 4.979,21.804 4.587,21.412 4.195,21.02 3.9993333,20.549333 4,20 V 4 C 4,3.45 4.196,2.979 4.588,2.587 4.98,2.195 5.4506667,1.9993333 6,2 h 8 l 6,6 v 12 c 0,0.55 -0.196,1.021 -0.588,1.413 C 19.02,21.805 18.549333,22.000667 18,22 Z M 13,9 h 5 L 13,4 Z m -6.299094,9.065324 h 1 v -2 h 1 c 0.283333,0 0.521,-0.096 0.713,-0.288 0.192,-0.192 0.287667,-0.429333 0.287,-0.712 v -1 c 0,-0.283334 -0.096,-0.521 -0.288,-0.713 -0.192,-0.192 -0.429333,-0.287667 -0.712,-0.287 h -2 z m 1,-3 v -1 h 1 v 1 z m 3,3 h 2 c 0.283333,0 0.520999,-0.096 0.712999,-0.288 0.192,-0.192 0.287667,-0.429333 0.287,-0.712 v -3 c 0,-0.283334 -0.096,-0.521 -0.288,-0.713 -0.192,-0.192 -0.429332,-0.287667 -0.711999,-0.287 h -2 z m 1,-1 v -3 h 1 v 3 z m 2.999999,1 h 1 v -2 h 1 v -1 h -1 v -1 h 1 v -1 h -2 z"
|
|
||||||
id="path1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,28 +1,233 @@
|
||||||
import { sequence } from "astro:middleware";
|
import { defineMiddleware, sequence } from "astro:middleware";
|
||||||
import { postProcessingMiddleware } from "src/middleware/postProcessing";
|
import { cache } from "src/utils/payload";
|
||||||
import { localeNegotiationMiddleware } from "src/middleware/languageNegotiation";
|
import acceptLanguage from "accept-language";
|
||||||
import { cookieRefreshingMiddleware } from "src/middleware/cookieRefreshing";
|
import type { AstroCookies } from "astro";
|
||||||
import { addCommonHeadersMiddleware } from "src/middleware/commonHeaders";
|
import { z } from "astro:content";
|
||||||
import { actionsHandlingMiddleware } from "src/middleware/actionsHandling";
|
import { trackRequest, trackEvent } from "src/shared/analytics/analytics";
|
||||||
import { requestTrackingMiddleware } from "src/middleware/requestTracking";
|
import { defaultLocale } from "src/i18n/i18n";
|
||||||
import { pageCachingMiddleware } from "src/middleware/pageCaching";
|
|
||||||
import { setAstroLocalsMiddleware } from "src/middleware/setAstroLocals";
|
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
|
||||||
import { removeTrailingSlashMiddleware } from "src/middleware/removeTrailingSlash";
|
|
||||||
|
const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
|
||||||
|
|
||||||
|
const redirect = (redirectURL: string, headers: Record<string, string> = {}): Response => {
|
||||||
|
return new Response(undefined, {
|
||||||
|
headers: { ...headers, Location: redirectURL },
|
||||||
|
status: 302,
|
||||||
|
statusText: "Found",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const localeAgnosticPaths = ["/api/"];
|
||||||
|
|
||||||
|
const localeNegotiator = defineMiddleware(({ cookies, url, request }, next) => {
|
||||||
|
if (localeAgnosticPaths.some((prefix) => url.pathname.startsWith(prefix))) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLocale = getCurrentLocale(url.pathname);
|
||||||
|
const acceptedLocale = getBestAcceptedLanguage(request);
|
||||||
|
const cookieLocale = getCookieLocale(cookies);
|
||||||
|
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? currentLocale ?? defaultLocale;
|
||||||
|
|
||||||
|
if (!currentLocale) {
|
||||||
|
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
|
||||||
|
trackEvent("locale-redirect");
|
||||||
|
return redirect(redirectURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLocale !== bestMatchingLocale) {
|
||||||
|
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
|
||||||
|
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
|
||||||
|
trackEvent("locale-redirect");
|
||||||
|
return redirect(redirectURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleActionsSearchParams = defineMiddleware(
|
||||||
|
async ({ url: { pathname, searchParams }, cookies }, next) => {
|
||||||
|
const language = searchParams.get("action-lang");
|
||||||
|
if (isValidLocale(language)) {
|
||||||
|
const currentLocale = getCurrentLocale(pathname);
|
||||||
|
const pathnameWithoutLocale = currentLocale
|
||||||
|
? pathname.substring(currentLocale.length + 1)
|
||||||
|
: pathname;
|
||||||
|
const redirectURL = getAbsoluteLocaleUrl(language, pathnameWithoutLocale);
|
||||||
|
trackEvent("action-lang");
|
||||||
|
cookies.set(CookieKeys.Language, language, {
|
||||||
|
maxAge: ninetyDaysInSeconds,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
return redirect(redirectURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currency = searchParams.get("action-currency");
|
||||||
|
if (isValidCurrency(currency)) {
|
||||||
|
trackEvent("action-currency");
|
||||||
|
cookies.set(CookieKeys.Currency, currency, {
|
||||||
|
maxAge: ninetyDaysInSeconds,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
return redirect(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = searchParams.get("action-theme");
|
||||||
|
if (isValidTheme(theme)) {
|
||||||
|
trackEvent("action-theme");
|
||||||
|
cookies.set(CookieKeys.Theme, theme, {
|
||||||
|
maxAge: theme === "auto" ? 0 : ninetyDaysInSeconds,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
return redirect(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshCookiesMaxAge = defineMiddleware(async ({ cookies }, next) => {
|
||||||
|
const response = await next();
|
||||||
|
|
||||||
|
const theme = cookies.get(CookieKeys.Theme)?.value;
|
||||||
|
if (isValidTheme(theme) && theme !== "auto") {
|
||||||
|
cookies.set(CookieKeys.Theme, theme, {
|
||||||
|
maxAge: ninetyDaysInSeconds,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
} else if (theme) {
|
||||||
|
cookies.set(CookieKeys.Theme, theme, {
|
||||||
|
maxAge: 0,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currency = cookies.get(CookieKeys.Currency)?.value;
|
||||||
|
if (isValidCurrency(currency)) {
|
||||||
|
cookies.set(CookieKeys.Currency, currency, {
|
||||||
|
maxAge: ninetyDaysInSeconds,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
} else if (currency) {
|
||||||
|
cookies.set(CookieKeys.Currency, currency, {
|
||||||
|
maxAge: 0,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = cookies.get(CookieKeys.Language)?.value;
|
||||||
|
if (isValidLocale(language)) {
|
||||||
|
cookies.set(CookieKeys.Language, language, {
|
||||||
|
maxAge: ninetyDaysInSeconds,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
} else if (language) {
|
||||||
|
cookies.set(CookieKeys.Language, language, {
|
||||||
|
maxAge: 0,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addContentLanguageResponseHeader = defineMiddleware(async ({ url }, next) => {
|
||||||
|
const currentLocale = getCurrentLocale(url.pathname);
|
||||||
|
|
||||||
|
const response = await next();
|
||||||
|
if (response.status === 200 && currentLocale) {
|
||||||
|
response.headers.set("Content-Language", currentLocale);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies }, next) => {
|
||||||
|
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
|
||||||
|
locals.currentCurrency = getCookieCurrency(cookies) ?? "USD";
|
||||||
|
locals.currentTheme = getCookieTheme(cookies) ?? "auto";
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const analytics = defineMiddleware(async (context, next) => {
|
||||||
|
const { request, params, locals, clientAddress } = context;
|
||||||
|
const response = await next();
|
||||||
|
trackRequest(request, { params, locals, clientAddress });
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
export const onRequest = sequence(
|
export const onRequest = sequence(
|
||||||
// Possible redirect
|
addContentLanguageResponseHeader,
|
||||||
actionsHandlingMiddleware,
|
handleActionsSearchParams,
|
||||||
localeNegotiationMiddleware,
|
refreshCookiesMaxAge,
|
||||||
removeTrailingSlashMiddleware,
|
localeNegotiator,
|
||||||
|
provideLocalsToRequest,
|
||||||
addCommonHeadersMiddleware,
|
analytics
|
||||||
|
|
||||||
// Get a response
|
|
||||||
requestTrackingMiddleware,
|
|
||||||
cookieRefreshingMiddleware,
|
|
||||||
setAstroLocalsMiddleware,
|
|
||||||
|
|
||||||
// Generate body
|
|
||||||
postProcessingMiddleware,
|
|
||||||
pageCachingMiddleware
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* LOCALE */
|
||||||
|
|
||||||
|
const getCurrentLocale = (pathname: string): string | undefined => {
|
||||||
|
for (const locale of cache.locales) {
|
||||||
|
if (pathname.split("/")[1] === locale.id) {
|
||||||
|
return locale.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBestAcceptedLanguage = (request: Request): string | undefined => {
|
||||||
|
const header = request.headers.get("Accept-Language");
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
acceptLanguage.languages(cache.locales.map(({ id }) => id));
|
||||||
|
|
||||||
|
return acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* COOKIES */
|
||||||
|
|
||||||
|
export enum CookieKeys {
|
||||||
|
Currency = "al_pref_currency",
|
||||||
|
Theme = "al_pref_theme",
|
||||||
|
Language = "al_pref_language",
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSchema = z.enum(["dark", "light", "auto"]);
|
||||||
|
|
||||||
|
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
|
||||||
|
const cookieValue = cookies.get(CookieKeys.Language)?.value;
|
||||||
|
return isValidLocale(cookieValue) ? cookieValue : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCookieCurrency = (cookies: AstroCookies): string | undefined => {
|
||||||
|
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
|
||||||
|
return isValidCurrency(cookieValue) ? cookieValue : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCookieTheme = (cookies: AstroCookies): z.infer<typeof themeSchema> | undefined => {
|
||||||
|
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
|
||||||
|
return isValidTheme(cookieValue) ? cookieValue : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidCurrency = (currency: string | null | undefined): currency is string =>
|
||||||
|
currency !== null && currency != undefined && cache.currencies.includes(currency);
|
||||||
|
|
||||||
|
export const isValidLocale = (locale: string | null | undefined): locale is string =>
|
||||||
|
locale !== null && locale != undefined && cache.locales.map(({ id }) => id).includes(locale);
|
||||||
|
|
||||||
|
export const isValidTheme = (
|
||||||
|
theme: string | null | undefined
|
||||||
|
): theme is z.infer<typeof themeSchema> => {
|
||||||
|
const result = themeSchema.safeParse(theme);
|
||||||
|
return result.success;
|
||||||
|
};
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import {
|
|
||||||
CookieKeys,
|
|
||||||
getAbsoluteLocaleUrl,
|
|
||||||
getCurrentLocale,
|
|
||||||
isValidCurrency,
|
|
||||||
isValidLocale,
|
|
||||||
isValidTheme,
|
|
||||||
redirect,
|
|
||||||
} from "src/middleware/utils";
|
|
||||||
import { analytics } from "src/services";
|
|
||||||
|
|
||||||
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
|
|
||||||
|
|
||||||
export const actionsHandlingMiddleware = defineMiddleware(async ({ url, cookies }, next) => {
|
|
||||||
const language = url.searchParams.get("action-lang");
|
|
||||||
if (isValidLocale(language)) {
|
|
||||||
const currentLocale = getCurrentLocale(url.pathname);
|
|
||||||
const pathnameWithoutLocale = currentLocale
|
|
||||||
? url.pathname.substring(currentLocale.length + 1)
|
|
||||||
: url.pathname;
|
|
||||||
url.pathname = getAbsoluteLocaleUrl(language, pathnameWithoutLocale);
|
|
||||||
url.searchParams.delete("action-lang");
|
|
||||||
analytics.trackEvent("action-lang");
|
|
||||||
cookies.set(CookieKeys.Language, language, {
|
|
||||||
maxAge: ninetyDaysInSeconds,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
return redirect(url.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
const currency = url.searchParams.get("action-currency");
|
|
||||||
if (isValidCurrency(currency)) {
|
|
||||||
analytics.trackEvent("action-currency");
|
|
||||||
cookies.set(CookieKeys.Currency, currency, {
|
|
||||||
maxAge: ninetyDaysInSeconds,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
url.searchParams.delete("action-currency");
|
|
||||||
return redirect(url.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = url.searchParams.get("action-theme");
|
|
||||||
if (isValidTheme(theme)) {
|
|
||||||
analytics.trackEvent("action-theme");
|
|
||||||
cookies.set(CookieKeys.Theme, theme, {
|
|
||||||
maxAge: theme === "auto" ? 0 : ninetyDaysInSeconds,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
url.searchParams.delete("action-theme");
|
|
||||||
return redirect(url.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
});
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { getCurrentLocale } from "src/middleware/utils";
|
|
||||||
|
|
||||||
export const addCommonHeadersMiddleware = defineMiddleware(async ({ url }, next) => {
|
|
||||||
const response = await next();
|
|
||||||
|
|
||||||
const currentLocale = getCurrentLocale(url.pathname);
|
|
||||||
if (currentLocale) {
|
|
||||||
response.headers.set("Content-Language", currentLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove when in production
|
|
||||||
response.headers.set("X-Robots-Tag", "none");
|
|
||||||
response.headers.set("Vary", "Cookie");
|
|
||||||
|
|
||||||
if (!response.headers.has("cache-control")) {
|
|
||||||
if (import.meta.env.CACHE_CONTROL !== "true" && !response.headers.has("cache-control")) {
|
|
||||||
response.headers.set("Cache-Control", "no-store");
|
|
||||||
} else {
|
|
||||||
response.headers.set("Cache-Control", "max-age=86400, stale-while-revalidate=86400");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
});
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { CookieKeys, isValidCurrency, isValidLocale, isValidTheme } from "src/middleware/utils";
|
|
||||||
|
|
||||||
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
|
|
||||||
|
|
||||||
export const cookieRefreshingMiddleware = defineMiddleware(async ({ cookies }, next) => {
|
|
||||||
const response = await next();
|
|
||||||
|
|
||||||
const theme = cookies.get(CookieKeys.Theme)?.value;
|
|
||||||
if (isValidTheme(theme) && theme !== "auto") {
|
|
||||||
cookies.set(CookieKeys.Theme, theme, {
|
|
||||||
maxAge: ninetyDaysInSeconds,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
} else if (theme) {
|
|
||||||
cookies.set(CookieKeys.Theme, theme, {
|
|
||||||
maxAge: 0,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const currency = cookies.get(CookieKeys.Currency)?.value;
|
|
||||||
if (isValidCurrency(currency)) {
|
|
||||||
cookies.set(CookieKeys.Currency, currency, {
|
|
||||||
maxAge: ninetyDaysInSeconds,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
} else if (currency) {
|
|
||||||
cookies.set(CookieKeys.Currency, currency, {
|
|
||||||
maxAge: 0,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const language = cookies.get(CookieKeys.Language)?.value;
|
|
||||||
if (isValidLocale(language)) {
|
|
||||||
cookies.set(CookieKeys.Language, language, {
|
|
||||||
maxAge: ninetyDaysInSeconds,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
} else if (language) {
|
|
||||||
cookies.set(CookieKeys.Language, language, {
|
|
||||||
maxAge: 0,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
});
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { defaultLocale } from "src/i18n/i18n";
|
|
||||||
import {
|
|
||||||
getAbsoluteLocaleUrl,
|
|
||||||
getBestAcceptedLanguage,
|
|
||||||
getCookieLocale,
|
|
||||||
getCurrentLocale,
|
|
||||||
redirect,
|
|
||||||
} from "src/middleware/utils";
|
|
||||||
import { analytics } from "src/services";
|
|
||||||
|
|
||||||
const localeAgnosticPaths = ["/api/"];
|
|
||||||
|
|
||||||
export const localeNegotiationMiddleware = defineMiddleware(({ cookies, url, request }, next) => {
|
|
||||||
if (localeAgnosticPaths.some((prefix) => url.pathname.includes(prefix))) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLocale = getCurrentLocale(url.pathname);
|
|
||||||
const acceptedLocale = getBestAcceptedLanguage(request);
|
|
||||||
const cookieLocale = getCookieLocale(cookies);
|
|
||||||
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? currentLocale ?? defaultLocale;
|
|
||||||
|
|
||||||
if (!currentLocale) {
|
|
||||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
|
|
||||||
analytics.trackEvent("locale-redirect");
|
|
||||||
return redirect(redirectURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLocale !== bestMatchingLocale) {
|
|
||||||
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
|
|
||||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
|
|
||||||
analytics.trackEvent("locale-redirect");
|
|
||||||
return redirect(redirectURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
});
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { pageCache } from "src/services";
|
|
||||||
|
|
||||||
export const pageCachingMiddleware = defineMiddleware(async ({ url, request, locals }, next) => {
|
|
||||||
const pathname = url.pathname;
|
|
||||||
const cachedPage = pageCache.get(pathname);
|
|
||||||
|
|
||||||
if (cachedPage) {
|
|
||||||
const clientTimestamp = request.headers.get("If-Modified-Since");
|
|
||||||
const serverTimestamp = cachedPage.headers.get("Last-Modified");
|
|
||||||
|
|
||||||
if (
|
|
||||||
clientTimestamp &&
|
|
||||||
serverTimestamp &&
|
|
||||||
new Date(clientTimestamp) == new Date(serverTimestamp)
|
|
||||||
) {
|
|
||||||
return new Response(null, { status: 304, statusText: "Not Modified" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await next();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
response.headers.set("Last-Modified", new Date().toUTCString());
|
|
||||||
|
|
||||||
if (locals.pageCaching) {
|
|
||||||
pageCache.set(pathname, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
});
|
|
|
@ -1,99 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { getI18n } from "src/i18n/i18n";
|
|
||||||
import { getCookieCurrency, getCookieTheme } from "src/middleware/utils";
|
|
||||||
import { convert } from "src/utils/currencies";
|
|
||||||
|
|
||||||
export enum PostProcessingTags {
|
|
||||||
HTML_CLASS = "POST_PROCESS_HTML_CLASS",
|
|
||||||
PRICE_START = "POST_PROCESS_PRICE_START",
|
|
||||||
PRICE_END = "POST_PROCESS_PRICE_END",
|
|
||||||
PREFERRED_CURRENCY = "POST_PROCESS_PREFERRED_CURRENCY",
|
|
||||||
CURRENCY_UNDERLINE_START = "POST_PROCESS_CURRENCY_UNDERLINE_START",
|
|
||||||
CURRENCY_UNDERLINE_END = "POST_PROCESS_CURRENCY_UNDERLINE_END",
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceRegex = new RegExp(
|
|
||||||
`${PostProcessingTags.PRICE_START}(.*?)${PostProcessingTags.PRICE_END}`,
|
|
||||||
"g"
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedCurrencyRegex = new RegExp(
|
|
||||||
`${PostProcessingTags.CURRENCY_UNDERLINE_START}(.*?)${PostProcessingTags.CURRENCY_UNDERLINE_END}`,
|
|
||||||
"g"
|
|
||||||
);
|
|
||||||
|
|
||||||
type PostProcessingCurrency = {
|
|
||||||
currency: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const prepareClassForSelectedCurrencyPostProcessing = (currency: PostProcessingCurrency) =>
|
|
||||||
`${PostProcessingTags.CURRENCY_UNDERLINE_START}${JSON.stringify(currency)}${PostProcessingTags.CURRENCY_UNDERLINE_END}`;
|
|
||||||
|
|
||||||
type PostProcessingPrice = {
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
format: "short" | "long";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatPriceForPostProcessing = (
|
|
||||||
price: Omit<PostProcessingPrice, "format">,
|
|
||||||
format: "short" | "long"
|
|
||||||
): string =>
|
|
||||||
`${PostProcessingTags.PRICE_START}${JSON.stringify({ ...price, format })}${PostProcessingTags.PRICE_END}`;
|
|
||||||
|
|
||||||
export const postProcessingMiddleware = defineMiddleware(async ({ cookies, locals }, next) => {
|
|
||||||
const { formatPrice, t } = await getI18n(locals.currentLocale);
|
|
||||||
const currentCurrency = getCookieCurrency(cookies) ?? "USD";
|
|
||||||
|
|
||||||
const response = await next();
|
|
||||||
let html = await response.text();
|
|
||||||
|
|
||||||
// HTML CLASS
|
|
||||||
const currentTheme = getCookieTheme(cookies) ?? "auto";
|
|
||||||
html = html.replace(
|
|
||||||
PostProcessingTags.HTML_CLASS,
|
|
||||||
currentTheme === "dark" ? "dark-theme" : currentTheme === "light" ? "light-theme" : ""
|
|
||||||
);
|
|
||||||
|
|
||||||
// PRICES
|
|
||||||
html = html.replaceAll(priceRegex, (_, priceString) => {
|
|
||||||
const unescapedString = priceString.replaceAll(""", '"');
|
|
||||||
const price = JSON.parse(unescapedString) as PostProcessingPrice;
|
|
||||||
|
|
||||||
if (price.amount === 0) {
|
|
||||||
return t("collectibles.price.free");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentCurrency === price.currency) {
|
|
||||||
return formatPrice(price);
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertedPrice = {
|
|
||||||
amount: convert(price.currency, currentCurrency, price.amount),
|
|
||||||
currency: currentCurrency,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (price.format === "long") {
|
|
||||||
return `${formatPrice(price)} (${formatPrice(convertedPrice)})`;
|
|
||||||
} else {
|
|
||||||
return formatPrice(convertedPrice);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PREFERRED_CURRENCY
|
|
||||||
html = html.replace(PostProcessingTags.PREFERRED_CURRENCY, currentCurrency.toUpperCase());
|
|
||||||
|
|
||||||
// SELECTED CURRENCY CLASS
|
|
||||||
html = html.replaceAll(selectedCurrencyRegex, (_, selectedCurrency) => {
|
|
||||||
const unescapedString = selectedCurrency.replaceAll(""", '"');
|
|
||||||
const currency = JSON.parse(unescapedString) as PostProcessingCurrency;
|
|
||||||
|
|
||||||
if (currentCurrency === currency.currency) {
|
|
||||||
return "current";
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(html, response);
|
|
||||||
});
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { redirect } from "src/middleware/utils";
|
|
||||||
|
|
||||||
export const removeTrailingSlashMiddleware = defineMiddleware(({ url }, next) => {
|
|
||||||
if (url.pathname.endsWith("/")) {
|
|
||||||
url.pathname = url.pathname.substring(0, url.pathname.length - 1);
|
|
||||||
return redirect(url.toString());
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
});
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { defineMiddleware } from "astro/middleware";
|
|
||||||
import { analytics } from "src/services";
|
|
||||||
|
|
||||||
export const requestTrackingMiddleware = defineMiddleware(async (context, next) => {
|
|
||||||
const { request, locals, clientAddress } = context;
|
|
||||||
const response = await next();
|
|
||||||
analytics.trackRequest(request, {
|
|
||||||
clientAddress,
|
|
||||||
locale: locals.currentLocale,
|
|
||||||
responseStatus: response.status,
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
});
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { defineMiddleware } from "astro:middleware";
|
|
||||||
import { getCurrentLocale } from "src/middleware/utils";
|
|
||||||
|
|
||||||
export const setAstroLocalsMiddleware = defineMiddleware(async ({ url, locals }, next) => {
|
|
||||||
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
|
|
||||||
locals.pageCaching = true;
|
|
||||||
return next();
|
|
||||||
});
|
|
|
@ -1,72 +0,0 @@
|
||||||
import type { AstroCookies } from "astro";
|
|
||||||
import { z } from "astro:content";
|
|
||||||
import { contextCache } from "src/services";
|
|
||||||
import acceptLanguage from "accept-language";
|
|
||||||
|
|
||||||
export const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
|
|
||||||
|
|
||||||
export const redirect = (redirectURL: string, headers: Record<string, string> = {}): Response => {
|
|
||||||
return new Response(undefined, {
|
|
||||||
headers: { ...headers, Location: redirectURL },
|
|
||||||
status: 302,
|
|
||||||
statusText: "Found",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/* LOCALE */
|
|
||||||
|
|
||||||
export const getCurrentLocale = (pathname: string): string | undefined => {
|
|
||||||
for (const locale of contextCache.locales) {
|
|
||||||
if (pathname.split("/")[1] === locale) {
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBestAcceptedLanguage = (request: Request): string | undefined => {
|
|
||||||
const header = request.headers.get("Accept-Language");
|
|
||||||
if (!header || header === "*") return;
|
|
||||||
|
|
||||||
acceptLanguage.languages(contextCache.locales);
|
|
||||||
|
|
||||||
return acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* COOKIES */
|
|
||||||
|
|
||||||
export enum CookieKeys {
|
|
||||||
Currency = "al_pref_currency",
|
|
||||||
Theme = "al_pref_theme",
|
|
||||||
Language = "al_pref_language",
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeSchema = z.enum(["dark", "light", "auto"]);
|
|
||||||
|
|
||||||
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
|
|
||||||
const cookieValue = cookies.get(CookieKeys.Language)?.value;
|
|
||||||
return isValidLocale(cookieValue) ? cookieValue : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCookieCurrency = (cookies: AstroCookies): string | undefined => {
|
|
||||||
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
|
|
||||||
return isValidCurrency(cookieValue) ? cookieValue : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCookieTheme = (cookies: AstroCookies): z.infer<typeof themeSchema> | undefined => {
|
|
||||||
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
|
|
||||||
return isValidTheme(cookieValue) ? cookieValue : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isValidCurrency = (currency: string | null | undefined): currency is string =>
|
|
||||||
currency !== null && currency != undefined && contextCache.currencies.includes(currency);
|
|
||||||
|
|
||||||
export const isValidLocale = (locale: string | null | undefined): locale is string =>
|
|
||||||
locale !== null && locale != undefined && contextCache.locales.includes(locale);
|
|
||||||
|
|
||||||
export const isValidTheme = (
|
|
||||||
theme: string | null | undefined
|
|
||||||
): theme is z.infer<typeof themeSchema> => {
|
|
||||||
const result = themeSchema.safeParse(theme);
|
|
||||||
return result.success;
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue