Cleaned up template
|
|
@ -0,0 +1,14 @@
|
||||||
|
; https://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
github: reednel
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn.lock
|
||||||
|
# package-lock.json
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
jspm_packages
|
||||||
|
**/node_modules/**
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache
|
||||||
|
**/.cache
|
||||||
|
|
||||||
|
# Additonal
|
||||||
|
.vscode
|
||||||
|
.astro
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
.output/
|
||||||
|
.json/
|
||||||
|
|
||||||
|
# Wrangler output
|
||||||
|
.wrangler/
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"MD033": false,
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-astro"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.astro"],
|
||||||
|
"options": {
|
||||||
|
"parser": "astro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2025 Reed Nelson
|
||||||
|
Copyright (c) 2025 Kristian Takvam
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
18
README.md
|
|
@ -1,2 +1,16 @@
|
||||||
# Kremtastic.github.io
|
|
||||||
Work in progress...
|
<div align="center">
|
||||||
|
<img src="src/assets/banner.svg" style="width:80%;" alt="Astrogon Logo" align="center" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[](https://github.com/astrogon/astrogon/blob/main/LICENSE) [](https://github.com/astrogon/astrogon)  [](https://astrogon.reednel.com/)
|
||||||
|
|
||||||
|
Heres the repo for my personal website!
|
||||||
|
|
||||||
|
**Work in progress** - Please check in later. I promise it will be worth it.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
###### *Built with the <a href="https://github.com/astrogon/astrogon" target="_blank">Astrogon</a> theme by <a href="https://reednel.com/" target="_blank">Reed Nelson</a> (MIT License)*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import mdx from "@astrojs/mdx";
|
||||||
|
import react from "@astrojs/react";
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
import tailwind from "@astrojs/tailwind";
|
||||||
|
import AutoImport from "astro-auto-import";
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import remarkCollapse from "remark-collapse";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import cloudflare from "@astrojs/cloudflare";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: "https://janedoe.com",
|
||||||
|
base: "/",
|
||||||
|
trailingSlash: "ignore",
|
||||||
|
prefetch: {
|
||||||
|
prefetchAll: true
|
||||||
|
},
|
||||||
|
adapter: cloudflare(),
|
||||||
|
integrations: [react(), sitemap(), tailwind({
|
||||||
|
config: {
|
||||||
|
applyBaseStyles: false
|
||||||
|
}
|
||||||
|
}), AutoImport({
|
||||||
|
imports: ["@components/common/Button.astro", "@shortcodes/Accordion", "@shortcodes/Notice", "@shortcodes/Youtube", "@shortcodes/Tabs", "@shortcodes/Tab"]
|
||||||
|
}), mdx()],
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [remarkToc, [remarkCollapse, {
|
||||||
|
test: "Table of contents"
|
||||||
|
}], remarkMath],
|
||||||
|
rehypePlugins: [[rehypeKatex, {}]],
|
||||||
|
shikiConfig: {
|
||||||
|
themes: { // https://shiki.style/themes
|
||||||
|
light: "light-plus",
|
||||||
|
dark: "dark-plus",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extendDefaultPlugins: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Docs
|
||||||
|
|
||||||
|
Notes
|
||||||
|
|
||||||
|
1. These docs are intended to be friendly and understandable for beginners, so anyone can rapidly assume ownership of this code.
|
||||||
|
2. Don't confuse these docs with the **Content Collection** called docs.
|
||||||
|
|
||||||
|
Recommended reading order:
|
||||||
|
|
||||||
|
1. [Tech Stack](./tech-stack.md) - Explains the choice of technologies used for this application, their limitations, and all the setup needed to get the site deployed to the internet.
|
||||||
|
2. [Project Structure](./project-structure.md) - Provides an overview of the project directory.
|
||||||
|
3. [Customization](./customization.md) - Points out the areas of highest impact for customization, and examples of this template's range.
|
||||||
|
|
||||||
|
## Git Quick Reference
|
||||||
|
|
||||||
|
### The Main Loop
|
||||||
|
|
||||||
|
Once you've made all the changes you want...
|
||||||
|
|
||||||
|
- Stage your changed files for commit: `git add .`
|
||||||
|
- Commit them, with a descriptive message: `git commit -m "message here"`
|
||||||
|
- Push the changes up to GitHub: `git push origin main`
|
||||||
|
|
||||||
|
### Other Useful Commands
|
||||||
|
|
||||||
|
If there's a change to any of your Node packages: `npm install`
|
||||||
|
If you mess things up real bad and just want to revert to the last commit: `git reset --hard`
|
||||||
|
When you want to pull changes down from GitHub: `git pull origin main`
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
[Astro Documentation](https://docs.astro.build) - The official documentation for Astro. If there's an Astro topic you're confused about, you can probably find a consise and clear explanation here.
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
# Customization Guide
|
||||||
|
|
||||||
|
## Content Collections
|
||||||
|
|
||||||
|
### Stock Content
|
||||||
|
|
||||||
|
The written content in the template's collections was AI generated. The images are Creative Commons licenced with a paint filter applied to them. The goal was to have disposable content that better demonstrates the form of the collections than "Lorem ipsum". In case you're unsure, review my [Terms of Use](https://astrogon.reednel.com/terms/) for the policy on the use and redistribution of this content.
|
||||||
|
|
||||||
|
### The Mechanics
|
||||||
|
|
||||||
|
The idea of a **Content Collection** is one of Astro's most central features. A content collection is a structured way to store and retrieve content for your site. Each collection consists of one or multiple **Entries**, which, in this site, are stored in a Markdown (md, mdx) format. At the top of any of these entry files, you will find **frontmatter**. This is space designed to hold any metadata you want to add, such as an entry's publication date, author, or whatever you want.
|
||||||
|
|
||||||
|
Adding an entry to a collection is as simple as creating a new file in the appropriate directory under `/src/content/`. For example, to add a new blog post on Bumble Bees, you would create a new file `/src/content/blog/bumble-bees.md`, and add all the necessary frontmatter. Then you can start plugging away in markdown. This is all very easy, and it's one of the best features of Astro. You can focus on writing content rather than worrying about the underlying code.
|
||||||
|
|
||||||
|
Things get a little more complicated when you want to create a new collection (or update an existing one). Here's a brief overview of how to do that. For examples, look at one of the dozen ways each of these steps are already implemented in this template.
|
||||||
|
|
||||||
|
1. The frontmatter we discussed must be defined in `/src/content/config.ts`. This file contains the schema for each collection.
|
||||||
|
2. To follow best Typescript practices, you should add your new collection type in `/src/content/config.ts`.
|
||||||
|
3. Add a new directory under `/src/content/` for your collection.
|
||||||
|
4. Lay out the structure of your entry's page. This will may consist of an `EntryLayout.astro` and a `CollectionLayout.astro` in the `/src/components/[collection]/` directory. These components will define how each entry and the collection as a whole will be displayed.
|
||||||
|
5. Connect the new collection to the routing structure by adding a new page in `/src/pages/` that will handle the routing for your collection. This typically involves calling `getEntries()` or `getIndex()` from the collection's config in the page's script section to retrieve the entries that will be referenced on that page.
|
||||||
|
|
||||||
|
For more context on how the content collections work, see the [/docs/project-structure.md](/docs/project-structure.md) page.
|
||||||
|
|
||||||
|
## Theme Colors
|
||||||
|
|
||||||
|
By default, the pallet of this site is defined in just 7 color parameters (14 if you count their dark theme variants).
|
||||||
|
|
||||||
|
| Name | CSS Label | Main Use |
|
||||||
|
| -------------------- | ----------- | ----------------------------------------------- |
|
||||||
|
| Text Primary | `txt-p` | header text |
|
||||||
|
| Text Secondary | `txt-s` | body text |
|
||||||
|
| Text Light | `txt-light` | indicate selected text |
|
||||||
|
| Background Primary | `bg-p` | main background (unless background is an image) |
|
||||||
|
| Background Secondary | `bg-s` | glass elements |
|
||||||
|
| Background Tertiary | `bg-t` | glass elements intended to contrast with `bg-s` |
|
||||||
|
| Border | `border` | border around glass elements |
|
||||||
|
|
||||||
|
### Relevant Areas
|
||||||
|
|
||||||
|
`/tailwind.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
...
|
||||||
|
theme: {
|
||||||
|
...
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
```
|
||||||
|
|
||||||
|
Structural changes would be made among:
|
||||||
|
|
||||||
|
- `/tailwind.config.js`
|
||||||
|
- `/src/styles/*`
|
||||||
|
- The inline tailwind classes in `/src/components/*`
|
||||||
|
|
||||||
|
## Fonts
|
||||||
|
|
||||||
|
External Resources:
|
||||||
|
|
||||||
|
- [Google Fonts](https://fonts.google.com/)
|
||||||
|
- [Mozilla Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)
|
||||||
|
- [Inter Font](https://rsms.me/inter/#)
|
||||||
|
- [Manrope Font](https://www.gent.media/manrope)
|
||||||
|
|
||||||
|
Generic font families that require no import:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
font-family: serif;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-family: monospace;
|
||||||
|
font-family: cursive;
|
||||||
|
font-family: fantasy;
|
||||||
|
font-family: system-ui; // uses the operating system's default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relevant Areas
|
||||||
|
|
||||||
|
`/tailwind.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
...
|
||||||
|
theme: {
|
||||||
|
...
|
||||||
|
extend: {
|
||||||
|
fontSize: {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `/src/styles/fonts.css`
|
||||||
|
|
||||||
|
All unused fonts referneces should be removed from this file. Likewise, any new fonts should be added here. This acts as the link between the `theme.json` and the actual font file stored in `/public/fonts/`.
|
||||||
|
|
||||||
|
#### `/public/fonts/`
|
||||||
|
|
||||||
|
Put font files here. It is recommended to store here only the font files that are actually used in the site. The more font files there are, the slower the site will load. This template comes with a few moderate options to experiment with, but you should remove any that you don't end up using.
|
||||||
|
|
||||||
|
A common alternative to this is to use a CDN to host the font files. This will work a little differently than shown here. Do not use a Google CDN, those dirty bastards can track your users that way.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
By default the background is a flat color defined by the `bg-p` color parameter discussed in the **Theme Colors** section.
|
||||||
|
|
||||||
|
### Relevant Areas
|
||||||
|
|
||||||
|
#### `/src/base/Background.astro`
|
||||||
|
|
||||||
|
Here I've provided three alternate kinds of background. All are commented out, but simply uncomment the desired one to use it. These are largely for demonstration purposes, and you should feel free to delete them and replace them with your own background.
|
||||||
|
|
||||||
|
This file is referenced only in `/src/base/BaseLayout.astro`.
|
||||||
|
|
||||||
|
#### `tailwind.config.js`
|
||||||
|
|
||||||
|
The provided **Twinkling Star Background** and **Gradient Cycle Background** depend on animations defined in this file.
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
animation: {
|
||||||
|
// Star Background
|
||||||
|
twinkle: "twinkle 5s infinite ease-in-out",
|
||||||
|
// Cycle Background
|
||||||
|
cycleBg: "cycleBg 15s ease infinite"
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
// Star Background
|
||||||
|
twinkle: {
|
||||||
|
"0%, 20%, 100%": { opacity: 1 },
|
||||||
|
"10%": { opacity: 0.25 },
|
||||||
|
},
|
||||||
|
// Cycle Background
|
||||||
|
cycleBg: {
|
||||||
|
"0%, 100%": { backgroundPosition: "0% 50%" },
|
||||||
|
"50%": { backgroundPosition: "100% 50%" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, there are code examples for single and dual-image backgrounds, depending on whether you want the background to change when the light/dark theme does. Swapping in your own images here is very straightforward.
|
||||||
|
|
||||||
|
### Parsimony
|
||||||
|
|
||||||
|
If you decide to use a solid color background, you can of course delete the code mentioned in the **Relevant Areas** section above.
|
||||||
|
|
||||||
|
## Glass Effect
|
||||||
|
|
||||||
|
One of the most powerful customization features of this template is the glass effect. By default, the template uses a true frosted glass effect, but by changing a few parameters specified below, the UI components can be given fully transparent or fully opaque backgrounds, for a more conventional website look.
|
||||||
|
|
||||||
|
| Name | CSS Label | Main Use |
|
||||||
|
| ------------------------- | -------------------- | ------------------------------------------------------- |
|
||||||
|
| Glass | `glass` | styles the background of most components |
|
||||||
|
| Glass Tertiary | `glass-t` | for glass atop other glass, resembles the use of `bg-t` |
|
||||||
|
| Glass Tertiary Borderless | `glass-t-borderless` | ui componnents with their own border |
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
| Name | Explanation |
|
||||||
|
| ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Color | The underlying background color of the element. |
|
||||||
|
| Opacity | `0` is fully transparent and `1` is fully opaque. If a color is specified, the default opacity is `1`, and if not, it's transparent. |
|
||||||
|
| Blur | The [blur](https://tailwindcss.com/docs/blur) is applied to the element's background, and the element itself is made semi-transparent. |
|
||||||
|
| Border | The border around the element. By default this effect is subtle, but adds a nice touch to the glass effect. |
|
||||||
|
| Shadow | The shadow around the element, giving it a bit of depth. |
|
||||||
|
|
||||||
|
### Relevant Areas
|
||||||
|
|
||||||
|
#### `/src/styles/glass.css`
|
||||||
|
|
||||||
|
## Animations
|
||||||
|
|
||||||
|
The animations are fairly subtle by default.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
| Animation | CSS |
|
||||||
|
| ---------- | -------------------------------------------- |
|
||||||
|
| Fade | `intersect:animate-fade opacity-0` |
|
||||||
|
| Fade Up | `intersect:animate-fadeUp opacity-0` |
|
||||||
|
| Fade Down | `intersect:animate-fadeDown opacity-0` |
|
||||||
|
| Fade Right | `intersect:animate-fadeRight opacity-0` |
|
||||||
|
| Fade Left | `intersect:animate-fadeLeft opacity-0` |
|
||||||
|
| Scale | `intersect:animate-scale opacity-0 scale-50` |
|
||||||
|
|
||||||
|
| Animation Trigger | CSS |
|
||||||
|
| ----------------------------- | -------------------- |
|
||||||
|
| Element fully in view | `intersect-full` |
|
||||||
|
| Element 50% in view | `intersect-half` |
|
||||||
|
| Element 25% in view (default) | `intersect-quarter` |
|
||||||
|
| Animate on every intersection | `intersect-repeat` |
|
||||||
|
| Animate element immediately | `intersect-no-queue` |
|
||||||
|
|
||||||
|
### Relevant Areas
|
||||||
|
|
||||||
|
`/tailwind.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
animation: {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
plugin(({ addVariant }) => {
|
||||||
|
addVariant("intersect", "&:not([no-intersect])");
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`/src/components/base/ObserverScript.astro`:
|
||||||
|
|
||||||
|
This is a segment of JS run as an inline `<script>` in the `Base` layout component. That is, it's Javascript code that's loaded on every page of the site. It functions as a custom CSS processor for the made-up class `intersect`. The simple idea of `intersect` is to trigger an animation whatever it's applied to, only when it enter's the browser's viewport (section of the screen that's visible). In the template, you can see this in action each time you load a page, with the various components fading into view, and, as you scroll further down a page, additional components become visible.
|
||||||
|
|
||||||
|
This functionality has some problems:
|
||||||
|
|
||||||
|
1. It's not quite like ordinary CSS. It cannot be extracted to the global classes in `/src/styles/`.
|
||||||
|
2. In its current form, it presents accessability issues. There is an accessability feature supported by browsers called `prefers-reduced-motion`. To be more friendly, these unnecessary animations should be disabled when this prefernece is detected.
|
||||||
|
3. By default, many of the animations include a "fade-in", bringing the opacity of an item from 0% (invisible) to 100% (fully opaque). This is a bit finnicky when crossing the already semi-opaque "Glass" objects. When not handled properly, the result is a discrete snap from 0% opacity to 100% opacity, which doesn't look good. In all cases thus far this issue has been circumnavigable, but keep this in minds for your own customizations: when editing an "intersect" item, making it behave right on an opaque background might make it behave poorly on a semi-opaque background, or vice-versa.
|
||||||
|
|
||||||
|
I think these animations make a big difference in the feel of the site, but they add a fair bit of weight to the pages and complexity to the customization. If you don't think they're worth the hassle, they're pretty easy to remove:
|
||||||
|
|
||||||
|
1. Ctrl-Shift-F for `intersect:animate` in the project and delete that and any CSS classes to the right of that on the given line. (Make sure you don't delete any unrelated classe you've added).
|
||||||
|
2. Delete the `<ObserverScript />` reference and import in `/src/components/base/BaseLayout.astro`
|
||||||
|
3. Delete `/src/components/base/ObserverScript.astro`
|
||||||
|
4. Delete the `theme.extent.animation` and `theme.extend.keyframes` sections from the `module.exports` section of `/tailwind.config.js`.
|
||||||
|
|
||||||
|
## Related Entries
|
||||||
|
|
||||||
|
The "Related Entries" section is a feature that automatically populates a list of links to other entries in the same collection. This is not a dedicated component, but just a small section of code that can be added to any entry layout.
|
||||||
|
|
||||||
|
By default in this template, there only example of this is in blog entries: `/src/components/blog/EntryLayout.astro`. You'll notice an array of entries called `relatedEntries` is is precomputed for each entry, and passed into the EntryLayout from the calling file: `/src/pages/blog/[entry].astro`. Here you'll see the `relatedEntries` array is populated by the function in `/src/lib/similarItems.ts`, where the logic actually sits.
|
||||||
|
|
||||||
|
The way `similarItems.ts` calculates what items are similar is just by reference to whatever metadata elements you choose to include in the comparison. In this template, that includes just the Categories and Tags. So if you want to add this Related Entries section to a content collection with other taxonomies, such as Diet for a recipe collection, you would want to update the `similarItems.ts` file to account for that.
|
||||||
|
|
||||||
|
## Favicon
|
||||||
|
|
||||||
|
The favicon for the site is stored in `/public/favicon/`. For best quality across a range of devices, you should supply the many versions, as you can see there in this template. [favicon.io](https://favicon.io/) is a great resource for generating favicons.
|
||||||
|
|
||||||
|
Note: if you change the favicon, you will likely need to clear your browser's cache to see the new one.
|
||||||
|
|
||||||
|
## Tool Tips
|
||||||
|
|
||||||
|
A tooltip is a small text box that appears when a user hovers over an element. Tooltips can provide additional information about an element without cluttering the UI. Astrogon implements [Astro Tooltips](https://github.com/JulianCataldo/web-garden/tree/develop/app/Tooltips). Read it's documentation if you want to get fancy with it, but the main thing is extremely straightforward. Just add the `title` attribute to any HTML element you want to have a tooltip. For example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
title="This is a tooltip!"
|
||||||
|
>
|
||||||
|
Click Me!
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Some customization levers are exposed in `/src/components/base/BaseLayout.astro` where `<Tooltips>` is called. Here you can specify the length of hover time before the tooltip appears, and how quickly it disappears after unhovering.
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
To make a collection searchable on the search page, you will need to:
|
||||||
|
|
||||||
|
1. Ensure the collection extends the `searchable` property in `/src/content/config.ts`. All that does is require the collection entries to have a title and description, otherwise there will be nothing to display in the search results.
|
||||||
|
2. Add the component to the union types comprising `SearchableEntry` in `/src/types/index.d.ts`. It's nice to keep Typescript happy.
|
||||||
|
3. Add the collection name to the `searchableCollections` array in `/src/pages/search.astro`. This tells the search algorithm which collections to actually query.
|
||||||
|
|
||||||
|
There's a built in `autodescription` feature. How this works is if an entry does not have a `description` in the frontmatter, it will automatically generate one from the first howevermany characters of the entry's body content.
|
||||||
|
|
||||||
|
The max length of the description is set in `/src/components/search/Search.tsx`. Change it if the default is upsetting to you.
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
Pagination refers to the process of dividing a large set of entries into smaller chunks or "pages". By default you can see this at work with the blog entries, where the `/blog` page will show a limited number of entries per page, and provide links to subsequent pages. This feature is fancier than it might seem, which is to say, adding it to a collection is not as easy as it seems like it should be.
|
||||||
|
|
||||||
|
Because each page of entries is, well, a page, you'll need to include a file `/src/pages/[collection]/page/[slug].astro`. This file will handle the routing for the pagination, and should look very similar to `/src/pages/[collection]/index.astro`. Of course, both will call that collection's `CollectionLayout` component. The pagination logic is handled by the `getIndex()` function in `/src/lib/pagination.ts`, which will return a slice of entries for the current page, as well as the total number of pages.
|
||||||
|
|
||||||
|
You can customize the number of entries per page by changing the `entriesPerPage` variable in both the `index.astro` and `[slug].astro` files mentioned above. Note also that the `getStaticPaths` function in `[slug].astro` operates in a special way with Astro, and build errors can occur if you define a variable outside of the function, for example.
|
||||||
|
|
||||||
|
I'm sick of writing about this without saying helpful things, so the TLDR is: just mimic how a working collection does it.
|
||||||
|
|
||||||
|
## Header and Footer
|
||||||
|
|
||||||
|
For the most part, these components will be customized within the component code directly. That is, `/src/components/base/Header.astro` and `/src/components/base/Footer.astro`.
|
||||||
|
|
||||||
|
To add or remove paths from the header, you can edit the `menu` array in `Header.astro`.
|
||||||
|
|
||||||
|
To add or remove social icons from the footer, you can edit values passed into the `<Social />` component in `Footer.astro`.
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
## ./
|
||||||
|
|
||||||
|
### Git/GitHub & Project Stuff
|
||||||
|
|
||||||
|
- `.git` is a hidden folder. It is the thing containing all the git tracking. It should never be editied manually. Basically you should just ignore it, it's hidden for a reason.
|
||||||
|
- `.github/` contains GitHub-specific files for the repository. If you want to use Github Pages as a host, you'd put a deployment script in here. However right now it's only purpose is to beg for your money. ...Probably just delete this folder.
|
||||||
|
- `.gitignore` is a file that tells git which files and folders not to track. Some of the stuff in the repository you only want to have locally. That could be really big assets, files with sensitive info like keys, etc. You likely won't need to edit this at all.
|
||||||
|
- `README.md` is the project README, of course. You can put whatever you want here, it will be displayed on the the repo's page on GitHub.
|
||||||
|
- `LICENSE` contains the license details. The MIT license is a good and honorable one. You should not use a more restrictive license than this on the repository overall. As a software license, it doen not apply to the actual content, such as your writings. I added a `/terms` page where you will be able to outline whatever permissions you want. I have something there for my own site, but I'm no lawyer.
|
||||||
|
|
||||||
|
### Node Stuff
|
||||||
|
|
||||||
|
- Recall, a huge portion of what allows the site to work is node stuff. Your site depends on hundreds of node packages to run.
|
||||||
|
- `package.json` is basically a list of your direct node dependencies. This is not meant to be edited directly.
|
||||||
|
- `package_lock.json` is basically a list of all your node dependencies. This is very much not meant to be edited directly.
|
||||||
|
- `node_modules` contains the dependencies listed in `package_lock.json`. This is what generated/updated when you run `npm install`. There's a lot of stuff in here, and NPM makes it easy to install, so we make sure not to track it with Git.
|
||||||
|
|
||||||
|
## src/
|
||||||
|
|
||||||
|
- This folder holds all of your site's source code, plus some other stuff.
|
||||||
|
- `assets/` is where you put all images that will be displayed on the site. Preferably, don't dump every image right in there, make subdirectories. For example, if you keep the blog content collection, make a folder `blog`, and put any images your blog uses in there. Even make one subdirectory per blog post.
|
||||||
|
- `content/` is where all your content collections live.
|
||||||
|
- For uniformity, a collection is defined for each "original content" driven page on the website, even if it's a solitary piece of content, such as the `terms` or `about` page.
|
||||||
|
- The data type for each content collection is defined in `/src/content/config.ts`. It will need to be updated if you want to edit the schema of a collection or add a new collection altogether.
|
||||||
|
- Once development is pretty much done on your site and you're happy with how everything looks and is structured, the expectation is that you will rarely need to interact with anything outside of this folder (and `assets`).
|
||||||
|
- `pages/` contains all the pages.
|
||||||
|
- The structure of this folder defines the routing structure for your site. So if in the `pages` folder you have `blog/categories/index.astro`, you will have that page `index.astro` located at `example.com/blog/categories`.
|
||||||
|
- The Docs collection actually breaks this rule, because its content is 2 layers deep. I implemented a workaround so the docs pages follow the structure of the `content/docs/` folder.
|
||||||
|
- The following are artificial constraints made to give a bit more structure and predictability to the astro code:
|
||||||
|
- Each file in `pages/` will directly reference exactly one page **layout**, and no other components.
|
||||||
|
- A **layout** is technically just a component, but to maintain a clear project structure, we prefer to treat them as the top-level component within a page.
|
||||||
|
- Almost all data is "queried" at this layer. For now, "querying" just refers to a function call, (e.g. `getIndex()` or `getEntries()`) that returns a freshly minted copy of one or multiple `CollectionEntry` objects (which are derived straight from the contents of `content/`).
|
||||||
|
- In this way, we decouple the routing and data querying from the structure/style of the pages themselves. You'd be encouraged to appreciate this design choice and stick to it, unless you specifically have other plans.
|
||||||
|
- `components/` contains all components, which are modular pieces of HTML/CSS/JS. There are three kinds of subdirectory:
|
||||||
|
1. `[collection]/` contains all layouts and other components specialized to that collection.
|
||||||
|
1. `EntryLayout` defines the structure of a collection entry page.
|
||||||
|
2. `CollectionLayout` defines the structure of the page(s) displaying the collection entries.
|
||||||
|
2. `common/` contains components that are generic enough to be used across collections.
|
||||||
|
- `shortcodes/` mainly contains components intended for use directly in `.mdx` content files.
|
||||||
|
3. `base/` contains the base layout and associated components. Every other layout in the project is wrapped by `base/BaseLayout.astro`. Therefore the base layout code is applied to every page on the site.
|
||||||
|
- TLDR:
|
||||||
|
- `content` holds the actual written content displayed to your site,
|
||||||
|
- `assets` holds the graphics and other resources referenced within `content`,
|
||||||
|
- `pages` dictates how the site will be traversed and where the data from `content` gets sent,
|
||||||
|
- `components` is where you give structure and style to the content.
|
||||||
|
|
||||||
|
## public/
|
||||||
|
|
||||||
|
- `favicon/` should hold the various forms of your favicon (different devices and contexts will prefer different versions). [Favicon.io](https://favicon.io/) is a great generator for simple favicons.
|
||||||
|
- `fonts/` holds the actual font files that all text on your site will be displayed in.
|
||||||
|
- Use the [Google Webfonts Helper](https://gwfh.mranftl.com/fonts) to download your fonts. You do not want to be downloading/importing more files than you actually need, so make sure you don't. There's more details here, but I will be happy to help if you need.
|
||||||
|
- Important note: Whenever you reference an image within the project, the root directory for it is assumed by Astro to be `public`. That means, for example, if you're writing a blog post and want to pull in an image `apple.png`, you'd reference it by just `"@assets/blog/apple.png"`, or whichever subdirectory you put it in.
|
||||||
|
- `CNAME` must hold the value `www.yourdomain.com`. Without going into detail, this is necessary to link your domain to the site, and get all the necessary certificates set up.
|
||||||
|
- `robots.txt` generally directs web-crawling bots. This one doesn't specify much, but it's fine how it is.
|
||||||
|
- `.htaccess` is also probably fine to be left as is.
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Tech Stack
|
||||||
|
|
||||||
|
This is a template for a static, content-driven website. Currently supported content collection types include a blog, recipe book, and documentation set. Using these as a guide you can create your own as well. While the architecture could be made to support dynamic content like user login, or a storefront, this template has made no steps in that direction.
|
||||||
|
|
||||||
|
## Justifying the Tech Stack
|
||||||
|
|
||||||
|
1. We want a static site.
|
||||||
|
1. There's no need for dynamic components.
|
||||||
|
2. Static sites are easier to build and maintain.
|
||||||
|
3. Static sites are easier to deploy and host.
|
||||||
|
4. Static sites are more secure and less breakable.
|
||||||
|
2. There are a handful of popular frameworks oriented around static site generation, e.g. Gatsby, Hugo, Jekyll. Most predate Astro, and I'm pretty sure all are worse. I refuse to elaborate.
|
||||||
|
3. Host Choice
|
||||||
|
1. A few places host your site for free, but many won't. Given that we're going to be static and low-traffic (no offense), we're prime candidates for free hosting.
|
||||||
|
2. Host choice in this framework isn't the biggest committment, and should not be that hard to change later if you want.
|
||||||
|
3. **GitHub Pages** and **Cloudflare Pages** are good options these days.
|
||||||
|
4. GitHub Pages Advantages
|
||||||
|
1. It is easy, you're already using Github.
|
||||||
|
2. Github is not a dominant tech giant in web-hosting.
|
||||||
|
5. Cloudflare Pages Advantages
|
||||||
|
1. offers more, and is also pretty easy to set up.
|
||||||
|
2. Letss you manage your domain and host a site on the same platform.
|
||||||
|
3. Let you host like 100 projects for free (Github lets you host 1).
|
||||||
|
4. As a dominant force in the space, they have a lot of fancy tooling and infrastructure that supposedly result in faster page load and higher security than Github can (or has any reason to) offer.
|
||||||
|
5. If you get to a point where you want to extend Astro beyond a fully static state, Cloudflare can support that, Github can't.
|
||||||
|
4. Isolating the development environment (optional)
|
||||||
|
1. If you have a special interest in isolating the software used to build your website locally, consider [Devbox](https://www.jetify.com/docs/devbox/).
|
||||||
|
2. This would alter your workflow and the setup steps as described in this documentation, so ignore this if you're unsure what this means.
|
||||||
|
3. Mimic [this PR](https://github.com/astrogon/astrogon/pull/30/commits/0f85e0c21e71a2fb00210f4588ed51d6398da163) from @fkromer for Devbox setup.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
1. Find and purchase a domain name from [Cloudflare](https://domains.cloudflare.com/) or someone.
|
||||||
|
- Don't do this until you're committed to your site of course.
|
||||||
|
- If you're planning to use GitHub Pages to host, you don't need your own domain, but it'd probably be better if you had one anyway.
|
||||||
|
2. Get a [GitHub](https://github.com/) account.
|
||||||
|
3. Download and install [VS Code](https://code.visualstudio.com/download).
|
||||||
|
4. Download and install [Git](https://git-scm.com/download/win).
|
||||||
|
5. Follow [this guide](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows) to install NVM and Node/NPM on Windows.
|
||||||
|
6. Acquire your own copy of the template repository.
|
||||||
|
- If you fork this repo, you could continue to pull updates to the `template` branch as they're made.
|
||||||
|
- If you don't want to deal with that, and want to completely cut Git ties to `reednel/pages`, this is acceptable.
|
||||||
|
7. Follow the repo's README to get the app up and running locally.
|
||||||
|
8. Follow Astro's guide for deploying a site to [GitHub Pages](https://docs.astro.build/en/guides/deploy/github/) or [Cloudflare Pages](https://docs.astro.build/en/guides/deploy/cloudflare/).
|
||||||
|
9. If using Github Pages, follow [this guide](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site?platform=windows) to get your domain name swapped in.
|
||||||
|
|
||||||
|
## But Why?
|
||||||
|
|
||||||
|
1. Git, Github
|
||||||
|
1. Git is a version control system, the most popular and I'd say best. Git tracks changes to files. It's kind of opaque but it's simple enough once you spend some time with it. GitHub is a platform over Git that makes it easier to look at and interact with, and stores things in clouds, and a bunch of other valuable things. Another thing is that GitHub Actions + GitHub Pages will make it so whatever we push up to GitHub is automatically built and deployed on your site.
|
||||||
|
2. Astro
|
||||||
|
1. If we weren't using a web framework such as Astro, we'd be committed to plain HTML/CSS/JS, with which it is much more difficult to make nice things. The benefit to simplicity is you won't have dependency issues basically ever. But the occasional maintainence is worth it.
|
||||||
|
3. Node, NVM, NPM
|
||||||
|
1. Node is Hell, don't forget it. The source of my hatred for web development. But it's necessary. With Astro (and most other frameworks) it's inescapable.
|
||||||
|
2. Node is Node, but NPM is the Node Package Manager. What Node actually does is...basically be a vast library of web tools (packages). But these things are small and numerous and conflict in unpredictable ways and depend on each other and specific versions in particular. This trememdous mess is what NPM handles. Albeit not very well sometimes.
|
||||||
|
3. It is likely that eventually you will need another version of Node. The Node Version Manager (NVM) makes the installation of and swapping between Node versions easy.
|
||||||
|
4. VS Code
|
||||||
|
1. VS Code is a lightweight but powerful code editor. Easily the best thing Microsoft ever made.
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"name": "Astrogon",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Template.",
|
||||||
|
"author": "Jane Doe",
|
||||||
|
"license": "MIT",
|
||||||
|
"packageManager": "npm@10.8.2",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"postbuild": "pagefind --site dist",
|
||||||
|
"format": "prettier -w ./src"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/cloudflare": "^12.6.4",
|
||||||
|
"@astrojs/mdx": "^4.3.4",
|
||||||
|
"@astrojs/react": "^4.3.0",
|
||||||
|
"@astrojs/rss": "^4.0.12",
|
||||||
|
"@astrojs/sitemap": "^3.5.0",
|
||||||
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
|
"astro": "^5.13.2",
|
||||||
|
"astro-auto-import": "^0.4.2",
|
||||||
|
"astro-tooltips": "^0.6.2",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
|
"date-fns-tz": "^2.0.0",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
|
"github-slugger": "^2.0.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"katex": "^0.16.21",
|
||||||
|
"marked": "^6.0.0",
|
||||||
|
"prettier-plugin-astro": "^0.11.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-lite-youtube-embed": "^2.3.52",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-collapse": "^0.1.2",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-toc": "^8.0.1",
|
||||||
|
"sharp": "^0.32.4",
|
||||||
|
"swiper": "^10.1.0",
|
||||||
|
"tailwind-merge": "^2.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@types/marked": "^5.0.1",
|
||||||
|
"@types/node": "20.4.8",
|
||||||
|
"@types/react": "18.2.18",
|
||||||
|
"@types/react-dom": "18.2.7",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.46.0",
|
||||||
|
"pagefind": "^1.0.4",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"prettier": "^3.0.1",
|
||||||
|
"prettier-plugin-astro": "^0.11.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
|
"sass": "^1.64.2",
|
||||||
|
"tailwind-bootstrap-grid": "^5.0.1",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "5.1.6",
|
||||||
|
"wrangler": "^4.24.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
_worker.js
|
||||||
|
_routes.json
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
##### Optimize default expiration time - BEGIN
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
|
||||||
|
## Enable expiration control
|
||||||
|
ExpiresActive On
|
||||||
|
|
||||||
|
## CSS and JS expiration: 1 week after request
|
||||||
|
ExpiresByType text/css "now plus 1 week"
|
||||||
|
ExpiresByType application/javascript "now plus 1 week"
|
||||||
|
ExpiresByType application/x-javascript "now plus 1 week"
|
||||||
|
|
||||||
|
## Image files expiration: 1 month after request
|
||||||
|
ExpiresByType image/bmp "now plus 1 month"
|
||||||
|
ExpiresByType image/gif "now plus 1 month"
|
||||||
|
ExpiresByType image/jpeg "now plus 1 month"
|
||||||
|
ExpiresByType image/webp "now plus 1 month"
|
||||||
|
ExpiresByType image/jp2 "now plus 1 month"
|
||||||
|
ExpiresByType image/pipeg "now plus 1 month"
|
||||||
|
ExpiresByType image/png "now plus 1 month"
|
||||||
|
ExpiresByType image/svg+xml "now plus 1 month"
|
||||||
|
ExpiresByType image/tiff "now plus 1 month"
|
||||||
|
ExpiresByType image/x-icon "now plus 1 month"
|
||||||
|
ExpiresByType image/ico "now plus 1 month"
|
||||||
|
ExpiresByType image/icon "now plus 1 month"
|
||||||
|
ExpiresByType text/ico "now plus 1 month"
|
||||||
|
ExpiresByType application/ico "now plus 1 month"
|
||||||
|
ExpiresByType image/vnd.wap.wbmp "now plus 1 month"
|
||||||
|
|
||||||
|
## Font files expiration: 1 month after request
|
||||||
|
ExpiresByType application/x-font-ttf "now plus 1 month"
|
||||||
|
ExpiresByType application/x-font-opentype "now plus 1 month"
|
||||||
|
ExpiresByType application/x-font-woff "now plus 1 month"
|
||||||
|
ExpiresByType font/woff2 "now plus 1 month"
|
||||||
|
ExpiresByType image/svg+xml "now plus 1 month"
|
||||||
|
|
||||||
|
## Audio files expiration: 1 month after request
|
||||||
|
ExpiresByType audio/ogg "now plus 1 month"
|
||||||
|
ExpiresByType application/ogg "now plus 1 month"
|
||||||
|
ExpiresByType audio/basic "now plus 1 month"
|
||||||
|
ExpiresByType audio/mid "now plus 1 month"
|
||||||
|
ExpiresByType audio/midi "now plus 1 month"
|
||||||
|
ExpiresByType audio/mpeg "now plus 1 month"
|
||||||
|
ExpiresByType audio/mp3 "now plus 1 month"
|
||||||
|
ExpiresByType audio/x-aiff "now plus 1 month"
|
||||||
|
ExpiresByType audio/x-mpegurl "now plus 1 month"
|
||||||
|
ExpiresByType audio/x-pn-realaudio "now plus 1 month"
|
||||||
|
ExpiresByType audio/x-wav "now plus 1 month"
|
||||||
|
|
||||||
|
## Movie files expiration: 1 month after request
|
||||||
|
ExpiresByType application/x-shockwave-flash "now plus 1 month"
|
||||||
|
ExpiresByType x-world/x-vrml "now plus 1 month"
|
||||||
|
ExpiresByType video/x-msvideo "now plus 1 month"
|
||||||
|
ExpiresByType video/mpeg "now plus 1 month"
|
||||||
|
ExpiresByType video/mp4 "now plus 1 month"
|
||||||
|
ExpiresByType video/quicktime "now plus 1 month"
|
||||||
|
ExpiresByType video/x-la-asf "now plus 1 month"
|
||||||
|
ExpiresByType video/x-ms-asf "now plus 1 month"
|
||||||
|
</IfModule>
|
||||||
|
##### Optimize default expiration time - END
|
||||||
|
|
||||||
|
##### 1 Month for most static resources
|
||||||
|
<filesMatch ".(css|jpg|jpeg|png|webp|gif|js|ico|woff|woff2|eot|ttf)$">
|
||||||
|
Header set Cache-Control "max-age=2592000, public"
|
||||||
|
</filesMatch>
|
||||||
|
|
||||||
|
##### Enable gzip compression for resources
|
||||||
|
<ifModule mod_gzip.c>
|
||||||
|
mod_gzip_on Yes
|
||||||
|
mod_gzip_dechunk Yes
|
||||||
|
mod_gzip_item_include file .(html?|txt|css|js|php)$
|
||||||
|
mod_gzip_item_include handler ^cgi-script$
|
||||||
|
mod_gzip_item_include mime ^text/.*
|
||||||
|
mod_gzip_item_include mime ^application/x-javascript.*
|
||||||
|
mod_gzip_item_exclude mime ^image/.*
|
||||||
|
mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
|
||||||
|
</ifModule>
|
||||||
|
|
||||||
|
##### Or, compress certain file types by extension:
|
||||||
|
<FilesMatch ".(html|css|jpg|jpeg|webp|png|gif|js|ico)">
|
||||||
|
SetOutputFilter DEFLATE
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
##### Set Header Vary: Accept-Encoding
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
<FilesMatch ".(js|css|xml|gz|html)$">
|
||||||
|
Header append Vary: Accept-Encoding
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
www.janedoe.com
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,6 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Disallow: /api/*
|
||||||
|
|
||||||
|
Sitemap: https://www.janedoe.com/sitemap-index.xml
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M39.7928 8C37.3152 8 36.3732 9.98312 36.565 13.825C36.6923 16.3576 37.5453 23.4183 33.9708 26.9962C30.6371 30.3316 29.6675 30.0353 25.8689 34.3423C22.0719 38.6477 18.907 37.7621 14.4764 37.3814C8.91051 36.9041 10.8067 67.8051 14.7295 67.2694C17.514 66.8887 23.8439 66.129 24.8564 66.129C25.8689 66.129 32.8125 71.5104 38.9076 71.5749C48.7799 71.6777 54.2245 69.9279 54.2245 69.9279L57.1179 63.3615L58.086 43.34L53.4636 33.7084C53.4636 33.7084 47.2611 34.7214 45.4891 31.9355C43.7172 29.1497 46.8821 25.7299 46.8821 17.7513C46.8821 9.77283 41.0585 8 39.7928 8Z" fill="url(#paint0_linear)"/>
|
||||||
|
<path d="M54.5697 69.8692L54.1939 68.4985C53.0816 68.3803 51.6503 68.8869 49.6958 68.8715C40.3605 68.7994 31.4363 66.1179 27.6071 63.0849C25.0988 61.0987 23.9988 58.4556 22.2269 59.2154C20.0407 60.1532 13.8382 61.6114 11.5569 58.7227C12.1951 63.6651 13.3334 67.4594 14.7295 67.2691C17.514 66.8884 23.8439 66.1286 24.8564 66.1286C25.8689 66.1286 32.6192 71.3688 38.9076 71.5745C47.0432 71.84 54.269 70.4386 54.269 70.4386L54.5697 69.8692Z" fill="url(#paint1_linear)"/>
|
||||||
|
<path d="M18.4298 39.3324C22.101 40.3454 24.4329 38.6693 26.3092 36.3884C28.1732 34.1229 29.7901 32.1136 34.4739 29.0745C39.1576 26.0353 38.5562 17.6762 38.1758 14.8903C37.7953 12.1044 38.0484 8 39.7928 8C37.3151 8 36.3732 9.98312 36.5649 13.825C36.6923 16.3577 37.5452 23.4183 33.9707 26.9962C30.637 30.3316 29.6674 30.0353 25.8689 34.3423C22.0719 38.6478 18.9069 37.7621 14.4763 37.3815C14.4779 38.3424 14.7586 38.3193 18.4298 39.3324Z" fill="url(#paint2_linear)"/>
|
||||||
|
<path d="M45.4891 31.9356C43.7172 29.1497 46.8821 25.7299 46.8821 17.7513C46.8821 9.77283 41.06 8 39.7928 8C39.4599 8 39.1638 8.03684 38.8984 8.10744C44.0624 8.43131 47.1721 14.947 45.3925 21.6654C43.9488 27.1174 43.2293 31.4889 45.5873 34.0522C47.1122 35.7115 50.1452 35.429 53.4651 33.7084C53.4636 33.7084 47.2611 34.7214 45.4891 31.9356Z" fill="url(#paint3_linear)"/>
|
||||||
|
<path d="M45.8373 34.7783C42.7997 35.7914 35.1274 38.1751 29.3161 43.4476C14.5883 56.809 39.7329 58.4836 44.926 69.9111C45.9186 72.0953 51.7207 58.456 51.7207 58.456L53.597 55.2971L48.1753 40.852L45.8373 34.7783Z" fill="url(#paint4_linear)"/>
|
||||||
|
<path d="M46.2837 35.9355C43.2461 36.9486 30.8364 44.9026 30.8364 48.8304C30.8364 60.1658 39.4307 54.571 44.0761 67.9464C44.7189 69.799 46.991 71.357 46.991 71.357C48.8734 69.8205 51.7223 58.4559 51.7223 58.4559L53.5985 55.2971L48.1769 40.8519L46.2837 35.9355Z" fill="url(#paint5_linear)"/>
|
||||||
|
<path d="M53.2948 70.6782L59.1092 64.0274L60.3902 36.2438L45.8358 34.7764C42.4944 36.5753 42.1078 39.0358 43.3489 42.9867C43.8706 44.649 41.7488 45.1248 41.6123 49.9675C41.5172 53.3244 43.7141 53.3397 43.7141 54.3374C43.7141 55.3351 42.6202 55.2461 42.2566 58.1364C41.7381 62.2715 45.1991 62.3513 45.1991 63.2354C45.1991 64.1195 43.2768 64.6444 43.395 68.0151C43.5453 72.2899 35.6107 71.4257 30.2458 67.5362C28.0428 65.9384 26.1205 65.7742 24.6201 66.1364C24.7137 66.1318 24.7935 66.1287 24.8533 66.1287C25.8658 66.1287 32.6667 71.5746 38.9045 71.5746C46.1994 71.5746 51.2437 71.1479 53.2948 70.6782Z" fill="url(#paint6_linear)"/>
|
||||||
|
<path d="M54.6325 36.6094C50.139 36.6094 47.6706 41.0023 49.3796 44.3776C51.0886 47.7529 46.2147 52.2211 46.7209 54.4206C47.2272 56.6202 48.1139 62.1873 48.8089 63.1083C49.5054 64.0292 47.669 66.1182 47.669 66.1182L53.3653 66.8151L58.6811 64.4697L58.4909 48.3884L59.06 38.3193L54.6325 36.6094Z" fill="url(#paint7_linear)"/>
|
||||||
|
<path d="M25.869 34.3424C23.4021 37.139 21.2021 37.7453 18.749 37.7223C19.4563 38.8643 19.9487 40.1122 19.8981 41.3325C19.7646 44.5313 20.4688 48.4745 21.0073 44.3732C21.4169 41.2496 23.534 37.3892 26.0807 34.1045C26.0117 34.1828 25.9426 34.2595 25.869 34.3424Z" fill="url(#paint8_linear)"/>
|
||||||
|
<path d="M58.0368 33.5827C52.7241 33.3801 43.1725 33.0792 42.9592 38.648C42.8058 42.6863 45.6685 44.011 50.0961 44.1798C54.5236 44.3487 56.7374 44.4331 58.5707 44.5022C60.404 44.5712 66.1724 44.5375 66.2292 39.661C66.2998 33.5689 58.0368 33.5827 58.0368 33.5827Z" fill="url(#paint9_linear)"/>
|
||||||
|
<path d="M49.2984 53.7131C47.8747 53.6716 43.645 54.7384 43.2492 57.4261C42.9378 59.5397 44.0163 62.2703 47.6967 62.915C52.359 63.7315 60.1279 64.1598 63.0305 63.0731C64.3268 62.588 65.4575 61.2296 65.5173 59.1713C65.5772 57.1129 64.0492 54.3393 60.7277 54.2426C57.4063 54.1475 55.6758 54.1981 53.7827 53.974C51.506 53.7069 50.4367 53.7468 49.2984 53.7131Z" fill="url(#paint10_linear)"/>
|
||||||
|
<path d="M47.8149 63.1064C46.2009 63.1064 44.2065 64.5309 44.2065 66.9054C44.2065 69.2799 44.8233 71.0374 48.2889 71.702C51.7545 72.3667 57.3081 71.7972 59.3961 71.1326C61.0238 70.6138 61.6268 69.0911 61.6268 66.5739C61.6268 64.0566 60.0619 63.616 58.3529 63.616C56.6439 63.616 54.5697 63.9276 53.3853 63.8662C50.0777 63.6913 49.4288 63.1064 47.8149 63.1064Z" fill="url(#paint11_linear)"/>
|
||||||
|
<path d="M57.5844 34.7063C52.9452 34.5297 44.6041 34.2611 44.4368 38.6633C44.3156 41.8559 46.8194 42.9119 50.6854 43.0593C54.5514 43.2066 64.7228 43.3923 64.7566 39.5382C64.798 34.7216 57.5844 34.7063 57.5844 34.7063Z" fill="url(#paint12_linear)"/>
|
||||||
|
<path d="M49.7264 54.8483C48.4868 54.813 45.0166 55.1077 44.6837 57.6756C44.4581 59.4132 45.3587 61.645 48.5635 62.1776C52.6244 62.853 59.3869 63.2152 61.9136 62.3296C63.0412 61.9335 64.0246 60.8238 64.0722 59.1369C64.1213 57.45 62.5672 55.3824 59.6769 55.298C56.7865 55.2136 55.2478 55.4822 53.5986 55.295C51.6165 55.0724 50.7174 54.8759 49.7264 54.8483Z" fill="url(#paint13_linear)"/>
|
||||||
|
<path d="M48.5052 63.9727C47.1091 63.9727 45.3848 65.1192 45.3848 67.0318C45.3848 68.9443 45.9187 70.3595 48.9164 70.8936C51.9141 71.4278 56.7175 70.9704 58.5247 70.4347C59.9315 70.0172 60.4547 68.7908 60.4547 66.7632C60.4547 64.7355 59.1 64.3825 57.6211 64.3825C56.1422 64.3825 54.3488 64.6342 53.324 64.5836C50.4613 64.4439 49.8998 63.9727 48.5052 63.9727Z" fill="url(#paint14_linear)"/>
|
||||||
|
<path d="M47.6538 38.6656C47.7689 35.6326 50.1867 34.179 53.2412 33.5098C48.3656 33.6234 43.1158 34.5351 42.9578 38.6472C42.8044 42.6856 45.6671 44.0102 50.0946 44.179C50.5825 44.1975 51.0412 44.2159 51.4769 44.2312C48.9916 43.5037 47.5326 41.8628 47.6538 38.6656Z" fill="url(#paint15_linear)"/>
|
||||||
|
<path d="M48.071 34.0586C45.2343 34.6787 43.0589 36.0202 42.9592 38.6465C42.8319 42.0034 44.791 43.4846 47.9988 43.9865C41.4911 41.3879 43.826 34.9872 48.071 34.0586Z" fill="url(#paint16_linear)"/>
|
||||||
|
<path d="M40.5169 65.9662C39.7667 67.6899 36.1967 67.7989 32.5424 66.2072C28.8881 64.617 26.5347 61.9294 27.2849 60.2042C28.0351 58.4804 31.605 58.3715 35.2594 59.9632C38.9122 61.5549 41.2671 64.2425 40.5169 65.9662Z" fill="url(#paint17_radial)"/>
|
||||||
|
<path d="M62.0165 39.4799C61.9797 41.81 58.8654 42.9151 55.458 43.1223C52.672 43.2911 49.5792 41.3188 49.659 39.0425C49.7388 36.7662 50.1438 34.7493 55.9336 34.9504C59.3456 35.0686 62.0518 37.2036 62.0165 39.4799Z" fill="url(#paint18_radial)"/>
|
||||||
|
<path d="M62.6347 59.9617C62.6148 63.1881 59.0694 62.5695 55.662 62.7936C51.3419 63.0791 48.4776 61.733 48.4761 59.4551C48.473 57.1773 50.1161 55.518 55.9106 55.5088C59.324 55.5042 62.6593 55.8174 62.6347 59.9617Z" fill="url(#paint19_radial)"/>
|
||||||
|
<path d="M59.5113 68.0967C59.4944 70.8473 56.2727 71.2218 53.2198 71.2234C50.1668 71.2264 46.8531 69.6071 46.8516 67.6654C46.85 65.7237 48.3182 64.3085 53.499 64.3008C56.5504 64.2962 59.5328 64.5633 59.5113 68.0967Z" fill="url(#paint20_radial)"/>
|
||||||
|
<path d="M47.2105 58.0802C47.6953 54.4071 50.512 54.622 53.7414 54.2413C50.0241 53.9635 48.4639 53.9236 48.4639 53.9236C48.1172 53.8898 47.729 53.8883 47.3256 53.9297C46.1182 54.1953 44.7497 54.7448 43.9366 55.7763C43.6605 56.1692 43.4365 56.6327 43.2892 57.1822C43.2739 57.2621 43.2585 57.3434 43.2493 57.4263C42.987 59.5476 44.0164 62.2705 47.6968 62.9152C48.2276 63.0088 48.7999 63.0963 49.3997 63.1777C49.6866 63.2145 49.9996 63.2529 50.3355 63.2959C50.4261 63.3066 50.5166 63.3174 50.6086 63.3281C48.5774 62.3688 46.853 60.7801 47.2105 58.0802Z" fill="url(#paint21_linear)"/>
|
||||||
|
<path d="M48.1093 53.9001C47.8592 53.8924 47.5938 53.9016 47.3223 53.9292C46.1042 54.1978 44.7235 54.755 43.915 55.8034C43.648 56.1902 43.4332 56.6476 43.289 57.1833C43.2737 57.2631 43.2629 57.3429 43.2491 57.4258C42.8763 59.5148 43.9104 61.9769 47.1259 62.7919C41.6398 59.765 43.9809 53.9031 48.1093 53.9001Z" fill="url(#paint22_linear)"/>
|
||||||
|
<path d="M51.3034 63.6672C49.6818 63.4293 49.016 63.1055 47.8163 63.1055C46.2024 63.1055 44.208 64.5299 44.208 66.9044C44.208 67.767 44.2909 68.5468 44.5593 69.2237C44.8324 69.7824 45.2298 70.3058 45.7744 70.7479C46.3727 71.1654 47.1858 71.4892 48.2888 71.701C49.0298 71.8438 49.8675 71.9282 50.745 71.9697C49.0513 70.9858 46.1825 70.4562 46.7838 67.1623C47.4481 63.5245 49.6189 64.0157 51.3034 63.6672Z" fill="url(#paint23_linear)"/>
|
||||||
|
<path d="M47.7442 63.1084C46.1441 63.1498 44.208 64.5666 44.208 66.9058C44.208 69.0808 44.7281 70.7385 47.4804 71.5121C43.1817 68.7492 44.5609 64.0877 47.7442 63.1084Z" fill="url(#paint24_linear)"/>
|
||||||
|
<path d="M66.2293 39.661C66.2355 39.1146 66.1741 38.6188 66.059 38.166C66.487 40.0693 64.7918 41.9604 60.2983 42.8782C57.3205 43.4861 49.412 44.2459 43.9397 42.0724C45.121 43.4999 47.2949 44.0709 50.0962 44.1783C54.5238 44.3472 56.7375 44.4316 58.5709 44.5007C60.4042 44.5728 66.171 44.5375 66.2293 39.661Z" fill="url(#paint25_linear)"/>
|
||||||
|
<path d="M65.5174 59.0422C65.5112 56.9977 63.9786 54.3376 60.7262 54.244C57.4048 54.1488 55.6743 54.1995 53.7812 53.9754C51.506 53.7068 50.4367 53.7467 49.2984 53.7145C48.4377 53.6899 46.3145 53.9493 44.8708 54.8472C46.494 55.4106 49.1388 56.2578 52.8607 56.0199C61.7787 55.4459 64.3161 56.5249 65.5174 59.0422Z" fill="url(#paint26_linear)"/>
|
||||||
|
<path d="M47.3854 44.5003C45.4877 44.4619 42.4731 45.5793 42.3949 49.5225C42.3166 53.4642 45.7638 54.4036 47.2826 54.4343C48.8015 54.465 54.1111 54.8272 60.6604 54.9577C67.2097 55.0881 68.0872 51.5716 68.124 49.7297C68.1608 47.8863 66.9933 44.554 63.0537 44.4742C51.6626 44.2455 47.3854 44.5003 47.3854 44.5003Z" fill="url(#paint27_linear)"/>
|
||||||
|
<path d="M48.4039 45.6439C46.7532 45.6117 44.136 46.4682 44.0761 49.5088C44.0148 52.551 47.014 53.2817 48.3334 53.3078C49.6543 53.3339 54.2705 53.624 59.9637 53.7375C65.6569 53.8511 66.4133 51.1405 66.4409 49.7176C66.47 48.2962 66.2737 45.7237 62.8479 45.6546C52.9481 45.4582 48.4039 45.6439 48.4039 45.6439Z" fill="url(#paint28_linear)"/>
|
||||||
|
<path d="M46.468 49.142C46.6582 45.0945 49.4826 45.0822 52.6706 44.3838C48.9426 44.4068 47.3854 44.4989 47.3854 44.4989C45.4877 44.4605 42.4731 45.578 42.3949 49.5212C42.3166 53.4628 45.7638 54.4022 47.2826 54.4329C47.8303 54.4437 48.872 54.4974 50.2834 54.568C48.1786 53.7008 46.3284 52.1198 46.468 49.142Z" fill="url(#paint29_linear)"/>
|
||||||
|
<path d="M64.7888 49.5613C64.9207 52.7846 61.3416 53.3909 57.9312 53.5536C54.5208 53.7163 50.7299 51.9926 50.621 49.7178C50.512 47.4431 52.0753 45.7087 57.8621 45.4278C61.2725 45.2635 64.62 45.4201 64.7888 49.5613Z" fill="url(#paint30_radial)"/>
|
||||||
|
<path d="M47.0308 44.5068C45.1346 44.6081 42.4683 45.8177 42.3946 49.5214C42.3195 53.2697 45.4307 54.3027 47.0431 54.4209C40.8037 51.9021 42.8488 44.8783 47.0308 44.5068Z" fill="url(#paint31_linear)"/>
|
||||||
|
<path d="M68.113 49.3311C67.2907 55.1637 51.8772 54.0954 43.5483 52.8429C44.6729 54.0478 46.3451 54.4131 47.2824 54.4331C48.8012 54.4638 54.1109 54.826 60.6602 54.9565C67.2094 55.087 68.087 51.5705 68.1238 49.7286C68.1253 49.6027 68.1222 49.4692 68.113 49.3311Z" fill="url(#paint32_linear)"/>
|
||||||
|
<path d="M60.3918 62.3194C57.4063 62.4515 49.507 63.4338 43.7478 60.3471C44.3906 61.5412 45.6348 62.5558 47.6952 62.9165C52.3574 63.7331 60.1263 64.1614 63.0289 63.0746C64.3253 62.5896 65.456 61.2312 65.5158 59.1729C65.324 60.9626 63.6196 62.1767 60.3918 62.3194Z" fill="url(#paint33_linear)"/>
|
||||||
|
<path d="M61.3982 68.8008C59.4422 70.2896 56.0609 71.1692 51.1823 70.3449C48.1693 69.8353 46.1043 69.6619 44.7466 69.625C45.2897 70.6074 46.3375 71.3288 48.2889 71.7033C51.7546 72.3679 57.3082 71.7985 59.3961 71.1338C60.4685 70.79 61.0944 70.0103 61.3982 68.8008Z" fill="url(#paint34_linear)"/>
|
||||||
|
<path d="M44.8479 64.7688C48.4623 63.4518 50.2343 65.4058 54.1019 65.4058C58.1183 65.4058 60.0329 63.7834 61.4596 65.326C61.0117 63.9031 59.7291 63.616 58.353 63.616C56.6439 63.616 54.5698 63.9276 53.3839 63.8662C50.0763 63.6928 49.4289 63.1064 47.8149 63.1064C46.7702 63.1064 45.5674 63.7035 44.8479 64.7688Z" fill="url(#paint35_linear)"/>
|
||||||
|
<path d="M14.7312 67.2694C17.5156 66.8887 23.8455 66.129 24.858 66.129C15.6777 59.5641 19.7601 45.6009 23.1459 38.5388C23.6215 37.5472 23.9145 36.7337 24.0726 36.043C21.0181 38.373 18.1922 37.7007 14.4765 37.3814C8.91061 36.9041 10.8083 67.8051 14.7312 67.2694Z" fill="url(#paint36_linear)"/>
|
||||||
|
<path d="M20.5193 60.9254C19.5697 59.6591 20.392 62.382 21.1529 64.3452C21.7988 66.0152 16.8451 65.0282 13.0542 65.4381C13.5497 66.6614 14.1158 67.3537 14.731 67.2692C17.5155 66.8886 23.8454 66.1288 24.8579 66.1288C23.4296 64.9147 21.4674 62.1917 20.5193 60.9254Z" fill="url(#paint37_linear)"/>
|
||||||
|
<path d="M24.8563 66.131C24.0693 65.4603 23.1197 64.3306 22.2651 63.2393C22.2651 63.2393 17.6566 65.5984 12.7151 64.4887C13.2843 66.2937 13.9685 67.3743 14.7294 67.2699C17.5154 66.8908 23.8438 66.131 24.8563 66.131Z" fill="url(#paint38_linear)"/>
|
||||||
|
<path d="M42.3138 19.8899C40.5096 20.2798 39.0476 17.7871 38.2222 14.8784C37.5472 12.4993 38.4646 9.46934 40.2365 9.12858C42.0085 8.7863 43.6439 8.77094 44.6211 13.8132C45.1949 16.7832 44.078 19.5092 42.3138 19.8899Z" fill="url(#paint39_radial)"/>
|
||||||
|
<path d="M47.7443 63.108C46.7517 63.2124 45.8527 63.745 45.2344 64.4772C44.6069 65.2155 44.3139 66.1794 44.3784 67.148C44.3845 68.1226 44.5318 69.1188 45.0841 69.9078C45.6318 70.7044 46.5522 71.1787 47.4804 71.5118C46.5231 71.2953 45.5213 70.8993 44.88 70.0597C44.2295 69.2293 44.0792 68.1472 44.047 67.1541C44.004 66.138 44.3277 65.0574 45.0442 64.3083C45.7391 63.5516 46.7425 63.0881 47.7443 63.108Z" fill="#804B24"/>
|
||||||
|
<path d="M46.152 54.2734C45.4984 54.5574 44.8955 54.9457 44.3969 55.4292C43.8891 55.9066 43.6191 56.5605 43.4335 57.2236L43.435 57.2128C43.1957 58.4223 43.3982 59.7101 44.1085 60.7155C44.8019 61.7347 45.928 62.4009 47.1246 62.7907C45.8927 62.5528 44.6516 61.9527 43.8722 60.8828C43.4872 60.3563 43.2202 59.7408 43.0913 59.0977C42.9609 58.4546 43.0208 57.7823 43.1404 57.1545L43.1435 57.1437C43.3383 56.4469 43.6851 55.7485 44.2573 55.2773C44.805 54.7907 45.4769 54.4914 46.152 54.2734Z" fill="#804B24"/>
|
||||||
|
<path d="M47.0309 44.5049C45.8573 44.6384 44.722 45.1265 43.9136 45.9477C43.102 46.7689 42.6632 47.9001 42.5927 49.0528C42.4745 50.2009 42.6602 51.3997 43.3168 52.3529C43.9642 53.3091 45.0227 53.9507 46.1549 54.2823C44.9951 54.0705 43.8414 53.4979 43.1081 52.5018C42.3595 51.5133 42.1616 50.2194 42.2613 49.0252C42.3595 47.8249 42.8304 46.6 43.7356 45.7666C44.6315 44.9239 45.8481 44.5172 47.0309 44.5049Z" fill="#804B24"/>
|
||||||
|
<path d="M48.0711 34.0586C46.8162 34.4024 45.5689 34.8767 44.5794 35.6918C43.5822 36.5022 43.0422 37.7532 43.1251 39.0348C43.1082 40.3073 43.5239 41.6058 44.4904 42.4485C45.4431 43.3019 46.7287 43.7164 47.9975 43.9865C46.7103 43.8346 45.371 43.5122 44.3232 42.6404C43.2508 41.7808 42.7952 40.3564 42.7937 39.0379C42.7246 37.6979 43.3276 36.295 44.4199 35.4938C45.4907 34.6726 46.7901 34.2858 48.0711 34.0586Z" fill="#804B24"/>
|
||||||
|
<path d="M43.7493 60.3457C44.613 61.8422 46.2729 62.6174 47.9129 62.8384C49.5821 63.0963 51.2635 63.2728 52.9495 63.3802C54.6325 63.523 56.3216 63.612 58.0107 63.6059C58.8544 63.5951 59.6997 63.5736 60.5389 63.4907C61.3781 63.4232 62.2249 63.3372 63.0319 63.0717C61.4349 63.6719 59.7028 63.767 58.0153 63.8269C56.3185 63.8699 54.6202 63.8223 52.9265 63.7118C51.2359 63.5659 49.5499 63.3526 47.8776 63.0564C47.0369 62.9182 46.1993 62.6573 45.4629 62.2091C44.7296 61.7563 44.1282 61.104 43.7493 60.3457Z" fill="#804B24"/>
|
||||||
|
<path d="M43.5483 52.8428C44.1957 53.4874 45.0288 53.9126 45.8986 54.1291C46.7639 54.3885 47.6737 54.3317 48.588 54.3731L54.048 54.5312L59.505 54.7998C61.3153 54.9103 63.1578 54.9349 64.8929 54.3409C63.2023 55.0623 61.303 55.1206 59.5004 55.0208L54.0404 54.8628L48.5834 54.5926C47.6859 54.5312 46.7424 54.5742 45.8542 54.2841C44.9766 54.0093 44.1559 53.5258 43.5483 52.8428Z" fill="#804B24"/>
|
||||||
|
<path d="M63.0535 44.4734C61.748 44.4765 60.4409 44.5133 59.1353 44.5011L55.2187 44.5087L51.3035 44.5625C49.9995 44.5701 48.6924 44.5778 47.393 44.63H47.3823C46.0982 44.6085 44.7681 45.0475 43.843 45.9669C42.8995 46.8771 42.4546 48.2018 42.3948 49.5218C42.4055 48.8602 42.4761 48.1925 42.6786 47.5525C42.8842 46.914 43.2355 46.3169 43.7126 45.8364C44.6638 44.8587 46.0353 44.3737 47.3884 44.3706H47.3777C49.9934 44.2125 52.606 44.211 55.2187 44.2524C57.8329 44.2816 60.444 44.3307 63.0535 44.4734Z" fill="#804B24"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear" x1="53.6692" y1="39.2608" x2="34.6268" y2="40.0249" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear" x1="33.1154" y1="65.6657" x2="32.7325" y2="55.6521" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear" x1="30.6742" y1="27.9693" x2="45.0193" y2="48.4791" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear" x1="20.2932" y1="19.7978" x2="57.0336" y2="22.5495" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.3118" stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FF8900"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint4_linear" x1="19.5935" y1="52.4637" x2="67.8399" y2="52.4637" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FFA754"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint5_linear" x1="11.8141" y1="52.5362" x2="107.13" y2="55.5726" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint6_linear" x1="26.959" y1="64.9917" x2="42.722" y2="56.4499" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint7_linear" x1="45.0161" y1="52.8643" x2="58.1604" y2="51.1381" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#643800" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#643800"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint8_linear" x1="21.6881" y1="23.8457" x2="24.5391" y2="71.0529" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FFA754"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint9_linear" x1="65.3286" y1="39.006" x2="46.6765" y2="39.006" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint10_linear" x1="65.3285" y1="58.7184" x2="46.6661" y2="58.7184" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint11_linear" x1="65.3286" y1="67.5521" x2="46.6765" y2="67.5521" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint12_linear" x1="44.162" y1="39.0984" x2="53.8772" y2="38.877" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint13_linear" x1="61.1514" y1="58.8499" x2="51.2483" y2="58.8499" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint14_linear" x1="61.1514" y1="67.5527" x2="51.2483" y2="67.5527" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint15_linear" x1="49.0629" y1="38.8702" x2="43.779" y2="38.8702" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FBA23B" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint16_linear" x1="48.1134" y1="39.0229" x2="42.0368" y2="39.0229" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#E89043"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint17_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(33.4032 61.7573) rotate(23.5251) scale(7.41808 3.44412)">
|
||||||
|
<stop stop-color="#FBE07A"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="paint18_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(55.8467 39.0525) rotate(2.02718) scale(6.34705 4.16577)">
|
||||||
|
<stop stop-color="#FBE07A"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="paint19_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(55.5394 59.5052) rotate(-0.0544055) scale(6.81633 3.67452)">
|
||||||
|
<stop stop-color="#FBE07A"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="paint20_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(53.1813 67.7665) rotate(-0.0539601) scale(6.24091 3.90602)">
|
||||||
|
<stop stop-color="#FBE07A"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint21_linear" x1="49.6959" y1="58.6129" x2="42.0377" y2="58.6129" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FBA23B" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint22_linear" x1="48.068" y1="58.3447" x2="43.9428" y2="58.3447" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint23_linear" x1="49.4205" y1="67.6029" x2="40.4645" y2="65.9582" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FBA23B" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint24_linear" x1="48.4945" y1="67.3101" x2="44.7089" y2="67.3101" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint25_linear" x1="54.529" y1="37.8317" x2="56.9364" y2="49.4826" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint26_linear" x1="56.3577" y1="60.8509" x2="53.7311" y2="51.3741" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint27_linear" x1="65.3287" y1="49.6691" x2="46.6766" y2="49.6691" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint28_linear" x1="45.924" y1="49.1486" x2="60.2119" y2="49.6705" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFCB4B"/>
|
||||||
|
<stop offset="0.7382" stop-color="#FFD748"/>
|
||||||
|
<stop offset="1" stop-color="#FFCB4B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint29_linear" x1="51.4997" y1="49.4764" x2="40.825" y2="49.4764" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FBA23B" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint30_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(57.6977 49.4978) rotate(-2.73606) scale(6.97949 4.58109)">
|
||||||
|
<stop stop-color="#FBE07A"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint31_linear" x1="46.5377" y1="49.4637" x2="43.1673" y2="49.4637" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.00132565" stop-color="#FFBC47"/>
|
||||||
|
<stop offset="1" stop-color="#FBA23B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint32_linear" x1="55.414" y1="48.4361" x2="57.4413" y2="61.3219" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint33_linear" x1="54.0091" y1="58.4048" x2="59.2033" y2="69.6743" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint34_linear" x1="52.9748" y1="68.2908" x2="53.735" y2="75.4145" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint35_linear" x1="53.1541" y1="67.2101" x2="53.1541" y2="62.057" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint36_linear" x1="18.3641" y1="52.9841" x2="2.21357" y2="77.3875" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#C86F34" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint37_linear" x1="17.2977" y1="58.3816" x2="19.9585" y2="70.254" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint38_linear" x1="18.9089" y1="60.6097" x2="18.5287" y2="75.142" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFBC47" stop-opacity="0"/>
|
||||||
|
<stop offset="1" stop-color="#C86F34"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint39_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(41.4453 14.4772) rotate(79.0714) scale(6.69229 3.92588)">
|
||||||
|
<stop stop-color="#FBE07A"/>
|
||||||
|
<stop offset="1" stop-color="#FFD748" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import { markdownify } from "@lib/textConverter";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import type { AboutEntry } from "@/types";
|
||||||
|
import { render } from "astro:content";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: AboutEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const { title, description, image, imageAlt } = entry.data;
|
||||||
|
const { Content } = await render(entry);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={title} description={description} image={image?.src}>
|
||||||
|
<section class="section-sm">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-center">
|
||||||
|
<div class="glass rounded-lg text-center p-8 md:col-10 lg:col-7 intersect:animate-fadeUp opacity-0 intersect-no-queue">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<Image
|
||||||
|
class="mx-auto mb-6 rounded-lg w-64"
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt}
|
||||||
|
width={1024}
|
||||||
|
height={1024}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<h2 set:html={markdownify(title)} class="h3 mb-6" />
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
import type { AuthorsEntry } from "@/types";
|
||||||
|
import { plainify } from "@lib/textConverter";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import Social from "@components/common/Social.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: AuthorsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry }: Props = Astro.props;
|
||||||
|
const { title, image, imageAlt, social } = entry.data;
|
||||||
|
const bodyBrief = entry.body ? plainify(entry.body?.slice(0, 100)) : null;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mb-14 md:col-6 lg:col-4">
|
||||||
|
<div
|
||||||
|
class="glass rounded-lg p-4 text-center intersect:animate-fadeUp opacity-0"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<Image
|
||||||
|
class="mx-auto mb-6 rounded"
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<a href={`/authors/${entry.id}`}>{title}</a>
|
||||||
|
</h4>
|
||||||
|
{bodyBrief && <p class="mb-4">{bodyBrief}</p>}
|
||||||
|
<Social links={social} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
import type { AuthorsEntry } from "@/types";
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import PageHeader from "@components/common/PageHeader.astro";
|
||||||
|
import AuthorCard from "@components/authors/Card.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
authorIndex: AuthorsEntry;
|
||||||
|
entries: AuthorsEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { authorIndex, entries } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={authorIndex.data.title} description={authorIndex.data.description}>
|
||||||
|
<PageHeader title={authorIndex.data.title} />
|
||||||
|
<section class="section-sm pb-0">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-center">
|
||||||
|
{entries.map((entry) => <AuthorCard entry={entry} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
import BlogCard from "@components/blog/Card.astro";
|
||||||
|
import Social from "@components/common/Social.astro";
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import { render } from "astro:content";
|
||||||
|
import type { AuthorsEntry, BlogEntry } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: AuthorsEntry;
|
||||||
|
recentBlogEntries: BlogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry, recentBlogEntries } = Astro.props;
|
||||||
|
const { title, social, description, image, imageAlt } = entry.data;
|
||||||
|
|
||||||
|
const { Content } = await render(entry);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={title} description={description} image={image?.src}>
|
||||||
|
<section class="section-sm pb-0">
|
||||||
|
<div class="container">
|
||||||
|
<div
|
||||||
|
class="row justify-center border-b border-border pb-14 dark:border-darkmode-border"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="glass rounded-lg p-4 text-center lg:col-4 intersect:animate-fadeUp opacity-0"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<Image
|
||||||
|
class="mx-auto mb-10 rounded"
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt}
|
||||||
|
height={200}
|
||||||
|
width={200}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<h1 class="h3 mb-6">{title}</h1>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
<Social links={social} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center pb-16 pt-14">
|
||||||
|
{
|
||||||
|
recentBlogEntries.map((entry) => (
|
||||||
|
<div class="mb-8 md:col-6 lg:col-4">
|
||||||
|
<BlogCard entry={entry} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<!-- ---
|
||||||
|
// Twinkling Star Background
|
||||||
|
const numStars = 1600;
|
||||||
|
const stars = Array.from({ length: numStars }).map(() => ({
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
size: `${Math.random() * 2 + 1}px`,
|
||||||
|
delay: `${7 * Math.random()}s`,
|
||||||
|
// hsl: `hsl(${Math.random() * 360}, 100%, 50%)`, // background: ${hsl};
|
||||||
|
}));
|
||||||
|
---
|
||||||
|
<div
|
||||||
|
class="fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
stars.map(({ left, top, size, delay }) => (
|
||||||
|
<div
|
||||||
|
class="absolute bg-none dark:bg-yellow-50 rounded-full animate-twinkle"
|
||||||
|
style={`
|
||||||
|
left: ${left};
|
||||||
|
top: ${top};
|
||||||
|
width: ${size};
|
||||||
|
height: ${size};
|
||||||
|
animation-delay: ${delay};
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Gradient Cycle Background -->
|
||||||
|
<!-- <div
|
||||||
|
class="gradient animate-cycleBg fixed top-0 left-0 w-full h-full -z-1 pointer-events-none overflow-hidden"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
.gradient {
|
||||||
|
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
}
|
||||||
|
</style> -->
|
||||||
|
|
||||||
|
<!-- Single Image Background -->
|
||||||
|
<!-- <style>
|
||||||
|
html::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: url(/src/assets/backgrounds/bwca-dusk.png) no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style> -->
|
||||||
|
|
||||||
|
<!-- Dual Image Background -->
|
||||||
|
<style>
|
||||||
|
html::before,
|
||||||
|
html::after {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity 0.25s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::before {
|
||||||
|
background-image: url(/src/assets/backgrounds/bwca-day.png);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark::before {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::after {
|
||||||
|
background-image: url(/src/assets/backgrounds/bwca-night.png);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
---
|
||||||
|
import { plainify } from "@lib/textConverter";
|
||||||
|
import Footer from "@components/base/Footer.astro";
|
||||||
|
import Header from "@components/base/Header.astro";
|
||||||
|
import ObserverScript from "@components/base/ObserverScript.astro";
|
||||||
|
import "@/styles/main.scss";
|
||||||
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
import Background from "@/components/base/Background.astro";
|
||||||
|
import { Tooltips } from "astro-tooltips";
|
||||||
|
|
||||||
|
// types for frontmatters
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string | null;
|
||||||
|
image?: string;
|
||||||
|
noindex?: boolean;
|
||||||
|
canonical?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, image, noindex, canonical } = Astro.props;
|
||||||
|
|
||||||
|
const siteTitle = "Astrogon";
|
||||||
|
const siteDescription = "Astrogon is a simple and elegant Astro theme.";
|
||||||
|
const siteImage = "@assets/og-image.png";
|
||||||
|
const siteAuthor = "Jane Doe";
|
||||||
|
const baseUrl = "https://janedoe.com";
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- favicon -->
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/favicon/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/favicon/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/favicon/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
|
||||||
|
<meta name="theme-name" content="pages" />
|
||||||
|
<meta name="msapplication-TileColor" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
content="#fff"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
content="#000"
|
||||||
|
/>
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>
|
||||||
|
{plainify(title ? title : siteTitle)}
|
||||||
|
</title>
|
||||||
|
|
||||||
|
{canonical && <link rel="canonical" href={canonical} item-prop="url" />}
|
||||||
|
{noindex && <meta name="robots" content="noindex,nofollow" />}
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content={plainify(description ? description : siteDescription)}
|
||||||
|
/>
|
||||||
|
<meta name="author" content={siteAuthor} />
|
||||||
|
<meta property="og:title" content={plainify(title ? title : siteTitle)} />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={plainify(description ? description : siteDescription)}
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta
|
||||||
|
property="og:url"
|
||||||
|
content={`${baseUrl}/${Astro.url.pathname.replace("/", "")}`}
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content={`${baseUrl}${image ? image : siteImage}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<meta name="twitter:title" content={plainify(title ? title : siteTitle)} />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content={plainify(description ? description : siteDescription)}
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:image"
|
||||||
|
content={`${baseUrl}${image ? image : siteImage}`}
|
||||||
|
/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<!-- Katex -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css"
|
||||||
|
integrity="sha384-zh0CIslj+VczCZtlzBcjt5ppRcsAmDnRem7ESsYwWwg3m/OaJ2l4x7YBZl9Kxxib"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"
|
||||||
|
integrity="sha384-Rma6DA2IPUwhNxmrB/7S3Tno0YY7sFu9WSYMCuulLhIqYSGZ2gKCJWIqhBWqMQfh"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/contrib/auto-render.min.js"
|
||||||
|
integrity="sha384-hCXGrW6PitJEwbkoStFjeJxv+fSOOQKOPbJxSfM6G5sWZjAyWhXiTIIAmQqnlLlh"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
onload="renderMathInElement(document.body);"></script>
|
||||||
|
|
||||||
|
<!-- https://docs.astro.build/en/guides/view-transitions -->
|
||||||
|
<ClientRouter fallback="swap" />
|
||||||
|
<Tooltips interactive={false} delay={[333, 0]} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Background />
|
||||||
|
<Header />
|
||||||
|
<main id="main-content" class="flex-1">
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<!-- <ObserverScript /> -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
import Social from "@components/common/Social.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="glass container lg:my-4 lg:rounded-lg intersect:animate-fadeUp opacity-0">
|
||||||
|
<div class="row py-2 lg:py-0 items-center flex-wrap-reverse">
|
||||||
|
<div class="text-center lg:col-6 lg:mb-0 lg:text-left">
|
||||||
|
<ul>
|
||||||
|
<li class="m-2 inline-block">
|
||||||
|
<a href="/terms">© Kristian Emil Takvam 2025</a> |
|
||||||
|
<a href="https://github.com/astrogon/astrogon" target="_blank"> <strong>Astrogon</strong></a> theme by
|
||||||
|
<a href="https://reednel.com/" target="_blank"> <strong>Reed Nelson</strong></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="text-center lg:col-6 lg:my-0 flex items-center justify-center lg:justify-end">
|
||||||
|
<Social
|
||||||
|
links={{
|
||||||
|
//facebook: "https://www.facebook.com/",
|
||||||
|
github: "https://www.github.com/Kremtastic",
|
||||||
|
//instagram: "https://www.instagram.com/",
|
||||||
|
linkedIn: "https://www.linkedin.com/in/kristian-emil-takvam-70a475215/",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
---
|
||||||
|
import ThemeSwitcher from "@components/base/ThemeSwitcher.astro";
|
||||||
|
import { IoSearch } from "react-icons/io5";
|
||||||
|
|
||||||
|
const { pathname } = Astro.url;
|
||||||
|
|
||||||
|
const stickyHeader = true;
|
||||||
|
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
|
name: "Home",
|
||||||
|
url: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "About",
|
||||||
|
url: "/about",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Docs",
|
||||||
|
url: "/docs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blog",
|
||||||
|
url: "/blog",
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
name: "Recipes",
|
||||||
|
url: "/recipes",
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
name: "Poetry",
|
||||||
|
url: "/poetry",
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
name: "Index Cards",
|
||||||
|
url: "/index-cards",
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
name: "Authors",
|
||||||
|
url: "/authors",
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
name: "Portfolio",
|
||||||
|
url: "/portfolio",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
transition:persist
|
||||||
|
class={`container glass py-2 z-30 mb-4 lg:mt-4 lg:rounded-lg ${stickyHeader && "sticky top-0"}`}
|
||||||
|
>
|
||||||
|
<nav class="relative flex flex-wrap items-center justify-between">
|
||||||
|
<!-- navbar toggler -->
|
||||||
|
<input id="nav-toggle" type="checkbox" class="hidden" />
|
||||||
|
<label
|
||||||
|
for="nav-toggle"
|
||||||
|
class="order-0 cursor-pointer flex items-center md:hidden text-txt-p dark:text-darkmode-txt-p lg:order-1"
|
||||||
|
>
|
||||||
|
<svg id="show-button" class="h-6 fill-current block" viewBox="0 0 20 20">
|
||||||
|
<title>Menu Open</title>
|
||||||
|
<path d="M0 3h20v2H0V3z m0 6h20v2H0V9z m0 6h20v2H0V0z"></path>
|
||||||
|
</svg>
|
||||||
|
<svg id="hide-button" class="h-6 fill-current hidden" viewBox="0 0 20 20">
|
||||||
|
<title>Menu Close</title>
|
||||||
|
<polygon
|
||||||
|
points="11 9 22 9 22 11 11 11 11 22 9 22 9 11 -2 11 -2 9 9 9 9 -2 11 -2"
|
||||||
|
transform="rotate(45 10 10)"></polygon>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<!-- /navbar toggler -->
|
||||||
|
<ul
|
||||||
|
id="nav-menu"
|
||||||
|
class="text-center lg:text-left order-3 hidden w-full pb-6 md:order-1 md:flex md:w-auto md:space-x-2 md:pb-0 lg:space-x-4"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
menu.map((item) => (
|
||||||
|
<li class="">
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
class={`block p-2 font-secondary font-semibold text-txt-p transition dark:text-darkmode-txt-p lg:px-2 lg:py-3 ${
|
||||||
|
(pathname === `${item.url}/` || pathname === item.url) &&
|
||||||
|
"active"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<div class="order-1 ml-auto flex items-center md:order-2 lg:ml-0">
|
||||||
|
<a
|
||||||
|
class="mr-4 inline-block border-border text-xl text-txt-p dark:border-darkmode-border dark:text-darkmode-txt-p"
|
||||||
|
href="/search"
|
||||||
|
aria-label="search"
|
||||||
|
>
|
||||||
|
<IoSearch />
|
||||||
|
</a>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<!-- Customized from https://github.com/heidkaemper/tailwindcss-intersect -->
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const Observer = {
|
||||||
|
observer: null,
|
||||||
|
delayBetweenAnimations: 100, // ms
|
||||||
|
animationCounter: 0,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const selectors = [
|
||||||
|
'[class*=" intersect:"]',
|
||||||
|
'[class*=":intersect:"]',
|
||||||
|
'[class^="intersect:"]',
|
||||||
|
'[class="intersect"]',
|
||||||
|
'[class*=" intersect "]',
|
||||||
|
'[class^="intersect "]',
|
||||||
|
'[class$=" intersect"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
const elements = Array.from(
|
||||||
|
document.querySelectorAll(selectors.join(","))
|
||||||
|
);
|
||||||
|
|
||||||
|
const getThreshold = (element) => {
|
||||||
|
if (element.classList.contains("intersect-full")) {
|
||||||
|
return 0.99;
|
||||||
|
} else if (element.classList.contains("intersect-half")) {
|
||||||
|
return 0.5;
|
||||||
|
} else if (element.classList.contains("intersect-quarter")) {
|
||||||
|
return 0.25;
|
||||||
|
} else {
|
||||||
|
return 0.25;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
el.setAttribute("no-intersect", "");
|
||||||
|
el._intersectionThreshold = getThreshold(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
const callback = (entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const target = entry.target;
|
||||||
|
const intersectionRatio = entry.intersectionRatio;
|
||||||
|
const threshold = target._intersectionThreshold;
|
||||||
|
|
||||||
|
if (target.classList.contains("intersect-no-queue")) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
target.removeAttribute("no-intersect");
|
||||||
|
if (!target.classList.contains("intersect-repeat")) {
|
||||||
|
this.observer.unobserve(target);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.setAttribute("no-intersect", "");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionRatio >= threshold) {
|
||||||
|
if (!target.hasAttribute("data-animated")) {
|
||||||
|
target.removeAttribute("no-intersect");
|
||||||
|
target.setAttribute("data-animated", "true");
|
||||||
|
|
||||||
|
const delay = this.animationCounter * this.delayBetweenAnimations;
|
||||||
|
this.animationCounter++;
|
||||||
|
|
||||||
|
target.style.animationDelay = `${delay}ms`;
|
||||||
|
|
||||||
|
if (!target.classList.contains("intersect-repeat")) {
|
||||||
|
this.observer.unobserve(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.setAttribute("no-intersect", "");
|
||||||
|
target.removeAttribute("data-animated");
|
||||||
|
target.style.transitionDelay = "";
|
||||||
|
target.style.animationDelay = "";
|
||||||
|
|
||||||
|
this.animationCounter = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver(callback.bind(this), {
|
||||||
|
threshold: [0, 0.25, 0.5, 0.99],
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
this.observer.observe(el);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Observer.start();
|
||||||
|
|
||||||
|
document.addEventListener("astro:after-swap", () => {
|
||||||
|
Observer.start();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script is:inline data-astro-rerun>
|
||||||
|
const defaultTheme = "system";
|
||||||
|
|
||||||
|
function getTheme(defaultTheme) {
|
||||||
|
var darkMode = defaultTheme === "dark" ? true : false;
|
||||||
|
if (localStorage.getItem("theme") === "system") {
|
||||||
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
darkMode = true;
|
||||||
|
}
|
||||||
|
} else if (localStorage.getItem("theme") === "dark") {
|
||||||
|
darkMode = true;
|
||||||
|
} else if (localStorage.getItem("theme") === "light") {
|
||||||
|
darkMode = false;
|
||||||
|
}
|
||||||
|
return darkMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(darkMode) {
|
||||||
|
// Update DOM
|
||||||
|
darkMode
|
||||||
|
? document.documentElement.classList.add("dark")
|
||||||
|
: document.documentElement.classList.remove("dark");
|
||||||
|
// Update localStorage
|
||||||
|
localStorage.setItem("theme", darkMode ? "dark" : "light");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This prevents flickering back to default theme before the page is fully loaded
|
||||||
|
setTheme(getTheme(defaultTheme));
|
||||||
|
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
setTheme(getTheme(defaultTheme));
|
||||||
|
// Theme switcher
|
||||||
|
var themeSwitch = document.querySelectorAll("[data-theme-switcher]");
|
||||||
|
themeSwitch.forEach((el) => {
|
||||||
|
el.addEventListener("change", (e) => {
|
||||||
|
setTheme(e.target.checked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
import ThemeScript from "@components/base/ThemeScript.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="inline-flex a mr-2">
|
||||||
|
<input
|
||||||
|
class="absolute opacity-0"
|
||||||
|
id="theme-switcher"
|
||||||
|
data-theme-switcher
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="relative inline-block h-4 w-6 cursor-pointer"
|
||||||
|
for="theme-switcher"
|
||||||
|
>
|
||||||
|
<span class="sr-only">theme switcher</span>
|
||||||
|
<span
|
||||||
|
class="absolute -top-1 left-0 flex h-6 w-6 items-center justify-center rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="absolute z-10 opacity-100 dark:opacity-0"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
height="21"
|
||||||
|
width="21"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#000"
|
||||||
|
d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="absolute z-10 opacity-0 dark:opacity-100"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
height="21"
|
||||||
|
width="21"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ThemeScript />
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
---
|
||||||
|
import type { BlogEntry } from "@/types";
|
||||||
|
import { upperHumanize, plainify, slugify } from "@lib/textConverter";
|
||||||
|
import {
|
||||||
|
FaRegFolder,
|
||||||
|
FaRegClock,
|
||||||
|
FaHashtag,
|
||||||
|
FaRegCalendarAlt,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import readingTime from "@lib/readingTime";
|
||||||
|
import { formatDate } from "@lib/formatDate";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: BlogEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry }: Props = Astro.props;
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
autodescription,
|
||||||
|
date,
|
||||||
|
categories,
|
||||||
|
tags,
|
||||||
|
complexity,
|
||||||
|
} = entry.data;
|
||||||
|
|
||||||
|
const descriptionLength = 200;
|
||||||
|
|
||||||
|
const entryDate = date ? formatDate(date) : null;
|
||||||
|
const entryReadingTime = readingTime(entry.body!, complexity);
|
||||||
|
const entryDescription =
|
||||||
|
description ||
|
||||||
|
(autodescription ? plainify(entry.body!.slice(0, descriptionLength)) : null);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="h-full m-2 pl-0 bg-gradient-to-br gradient rounded-lg">
|
||||||
|
<div class="glass h-full rounded-lg p-4 intersect:animate-fadeUp opacity-0">
|
||||||
|
<h4 class="mb-2">
|
||||||
|
<a href={`/blog/${entry.id}`}>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<ul class="mb-2">
|
||||||
|
{
|
||||||
|
categories && (
|
||||||
|
<li class="mr-0 inline-block">
|
||||||
|
{categories.map((category: string, index: number) => (
|
||||||
|
<a href={`/blog/categories/${slugify(category)}`}>
|
||||||
|
<FaRegFolder className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{upperHumanize(category)}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<!-- { tags && (
|
||||||
|
<li class="mr-0 inline-block">
|
||||||
|
{
|
||||||
|
tags.map((tag: string, index: number) => (
|
||||||
|
<a href={`/blog/tags/${slugify(tag)}`}>
|
||||||
|
<FaHashtag className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{lowerHumanize(tag)}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
)} -->
|
||||||
|
{
|
||||||
|
entryDate && (
|
||||||
|
<li class="mr-0 inline-block">
|
||||||
|
<FaRegCalendarAlt className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{entryDate}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
complexity > 0 && (
|
||||||
|
<li class="mr-0 inline-block">
|
||||||
|
<FaRegClock className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{entryReadingTime}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<p class="">
|
||||||
|
{entryDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import BlogCard from "@components/blog/Card.astro";
|
||||||
|
import Pagination from "@components/common/Pagination.astro";
|
||||||
|
import PageHeader from "@components/common/PageHeader.astro";
|
||||||
|
import BlogSidebar from "@components/blog/Sidebar.astro";
|
||||||
|
import type { BlogEntry } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entryIndex: BlogEntry;
|
||||||
|
entries: BlogEntry[];
|
||||||
|
tags: string[];
|
||||||
|
categories: string[];
|
||||||
|
allCategories: string[];
|
||||||
|
pageIndex: number;
|
||||||
|
pageCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
entryIndex,
|
||||||
|
entries,
|
||||||
|
tags,
|
||||||
|
categories,
|
||||||
|
allCategories,
|
||||||
|
pageIndex,
|
||||||
|
pageCount,
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={entryIndex.data.title} description={entryIndex.data.description}>
|
||||||
|
<PageHeader title={entryIndex.data.title} />
|
||||||
|
<section class="section">
|
||||||
|
<div class="flex container">
|
||||||
|
<div class="row px-0 mx-0 w-full">
|
||||||
|
<!-- blog posts -->
|
||||||
|
<div class="col-12 lg:col-8 lg:p-0">
|
||||||
|
{
|
||||||
|
entries.map((entry) => (
|
||||||
|
<div class="mb-4">
|
||||||
|
<BlogCard entry={entry} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Pagination
|
||||||
|
collection="blog"
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageCount={pageCount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- sidebar -->
|
||||||
|
<div
|
||||||
|
class="hidden lg:col-4 lg:flex max-h-static_sidemenu sticky p-0 pl-4 top-[4rem]"
|
||||||
|
>
|
||||||
|
<BlogSidebar
|
||||||
|
tags={tags}
|
||||||
|
categories={categories}
|
||||||
|
allCategories={allCategories}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import BlogCard from "@components/blog/Card.astro";
|
||||||
|
import Share from "@components/common/Share.astro";
|
||||||
|
import { plainify } from "@lib/textConverter";
|
||||||
|
import type { BlogEntry } from "@/types";
|
||||||
|
import TableOfContents from "@components/common/TableOfContents.astro";
|
||||||
|
import { render } from "astro:content";
|
||||||
|
import EntryHeader from "@components/common/EntryHeader.astro";
|
||||||
|
interface Props {
|
||||||
|
entry: BlogEntry;
|
||||||
|
relatedEntries: BlogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry, relatedEntries }: Props = Astro.props;
|
||||||
|
const { title, description, autodescription, image, hideToc } = entry.data;
|
||||||
|
const { Content, headings } = await render(entry);
|
||||||
|
|
||||||
|
const descriptionLenth = 200; // the max length in characters for the auto-generated description
|
||||||
|
const globalHideToc = false; // set true to always hide the table of contents (right side menu)
|
||||||
|
const tocDepth = 3; // maximum depth for the table of contents 1 = h1, etc
|
||||||
|
|
||||||
|
const actuallyHideToc = hideToc || globalHideToc || headings.filter((heading) => heading.depth <= tocDepth).length === 0;
|
||||||
|
|
||||||
|
const entryDescription =
|
||||||
|
description ||
|
||||||
|
(autodescription ? plainify(entry.body!.slice(0, descriptionLenth)) : null);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={title} description={entryDescription} image={image?.src}>
|
||||||
|
<section class="flex container p-4">
|
||||||
|
<div class=`w-full ${actuallyHideToc ? "" : "md:col-9"}`>
|
||||||
|
<article class="">
|
||||||
|
<section>
|
||||||
|
<EntryHeader
|
||||||
|
entry={entry}
|
||||||
|
showImage
|
||||||
|
showAuthor
|
||||||
|
showDate
|
||||||
|
showReadingTime
|
||||||
|
showCategories
|
||||||
|
showTags
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section class="content mb-4 glass px-4 rounded-lg">
|
||||||
|
<Content />
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class=`hidden max-h-static_sidemenu sticky top-[5rem] pl-4 ${globalHideToc || hideToc ? "" : "md:flex md:col-3"}`
|
||||||
|
>
|
||||||
|
<TableOfContents {headings} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class=`container items-start justify-between mt-4`>
|
||||||
|
<hr />
|
||||||
|
<div class="flex items-center justify-center lg:justify-end mt-4">
|
||||||
|
<Share
|
||||||
|
title={title}
|
||||||
|
description={entryDescription}
|
||||||
|
folder={"blog"}
|
||||||
|
id={entry.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Related posts -->
|
||||||
|
{
|
||||||
|
relatedEntries.length > 0 && (
|
||||||
|
<section class="row justify-center my-8">
|
||||||
|
<h2 class="h3 mb-4 text-center">Related Posts</h2>
|
||||||
|
<div class="lg:col-10">
|
||||||
|
<div class="row justify-center">
|
||||||
|
{relatedEntries.slice(0, 2).map((entry) => (
|
||||||
|
<div class="md:col-6 px-4 py-2 h-min">
|
||||||
|
<BlogCard entry={entry} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
---
|
||||||
|
import { lowerHumanize, upperHumanize } from "@lib/textConverter";
|
||||||
|
import { FaRegFolder, FaHashtag } from "react-icons/fa";
|
||||||
|
import Button from "@components/common/Button.astro";
|
||||||
|
import { ScrollArea } from "@components/common/shortcodes/ScrollArea";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tags: string[];
|
||||||
|
categories: string[];
|
||||||
|
allCategories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tags, categories, allCategories }: Props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ScrollArea className="h-full" client:only="react">
|
||||||
|
<!-- categories -->
|
||||||
|
<div class="glass rounded-lg p-4 m-2 mb-4 intersect:animate-fadeLeft opacity-0">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">
|
||||||
|
<a href="/blog/categories">
|
||||||
|
<FaRegFolder className={"mr-1 -mt-1.5 inline-block"} />
|
||||||
|
Categories
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<hr class="mb-2 -mt-1" />
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{
|
||||||
|
categories.map((category) => {
|
||||||
|
const count = allCategories.filter((c) => c === category).length;
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class=""
|
||||||
|
href={`/blog/categories/${category}`}
|
||||||
|
>
|
||||||
|
{upperHumanize(category)}{" "}
|
||||||
|
<div class="inline-block pl-1">({count})</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- tags -->
|
||||||
|
<div class="glass rounded-lg p-4 m-2 intersect:animate-fadeLeft opacity-0">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">
|
||||||
|
<a href="/blog/tags">
|
||||||
|
<FaHashtag className={"mr-1 -mt-1.5 inline-block"} />
|
||||||
|
Tags
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<hr class="mb-2 -mt-1" />
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
tags.map((tag: any) => {
|
||||||
|
return (
|
||||||
|
<li class="inline-block m-1">
|
||||||
|
<Button
|
||||||
|
label={tag}
|
||||||
|
link={`/blog/tags/${lowerHumanize(tag)}`}
|
||||||
|
type="secondary"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import { upperHumanize } from "@lib/textConverter";
|
||||||
|
import PageHeader from "@components/common/PageHeader.astro";
|
||||||
|
import Button from "@components/common/Button.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
taxonomy: string;
|
||||||
|
taxa: string[];
|
||||||
|
allTaxa: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { taxonomy, taxa, allTaxa }: Props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={upperHumanize(taxonomy)}>
|
||||||
|
<PageHeader title={upperHumanize(taxonomy)} />
|
||||||
|
<section class="section">
|
||||||
|
<div class="container text-center">
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
taxa.map((taxon: string) => {
|
||||||
|
const count = allTaxa.filter((c) => c === taxon).length;
|
||||||
|
return (
|
||||||
|
<li class="m-3 inline-block">
|
||||||
|
<Button
|
||||||
|
label=`${upperHumanize(taxon)} (${count})`
|
||||||
|
link={`/blog/${taxonomy}/${taxon}`}
|
||||||
|
hoverInvert
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import PageHeader from "@components/common/PageHeader.astro";
|
||||||
|
import { upperHumanize } from "@lib/textConverter";
|
||||||
|
import BlogCard from "@components/blog/Card.astro";
|
||||||
|
import type { BlogEntry } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
taxon: string;
|
||||||
|
entries: BlogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { taxon, entries }: Props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={upperHumanize(taxon)}>
|
||||||
|
<PageHeader title={taxon} />
|
||||||
|
<section class="section-sm">
|
||||||
|
<div class="pb-0">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
{
|
||||||
|
entries.map((entry) => (
|
||||||
|
<div class="py-2 md:py-4 md:col-6">
|
||||||
|
<BlogCard entry={entry} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
import { lowerHumanize } from "@lib/textConverter";
|
||||||
|
|
||||||
|
const { className }: { className?: string } = Astro.props;
|
||||||
|
|
||||||
|
const paths = Astro.url.pathname.split("/").filter((x) => x);
|
||||||
|
let parts = [
|
||||||
|
{
|
||||||
|
label: "Home",
|
||||||
|
href: "/",
|
||||||
|
"aria-label": Astro.url.pathname === "/" ? "page" : undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
paths.forEach((label: string, i: number) => {
|
||||||
|
const href = `/${paths.slice(0, i + 1).join("/")}`;
|
||||||
|
label !== "page" &&
|
||||||
|
parts.push({
|
||||||
|
label: lowerHumanize(label.replace(/[-_]/g, " ")) || "",
|
||||||
|
href,
|
||||||
|
"aria-label": Astro.url.pathname === href ? "page" : undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<nav aria-label="Breadcrumb" class={className}>
|
||||||
|
<ol class="inline-flex" role="list">
|
||||||
|
{
|
||||||
|
parts.map(({ label, ...attrs }, index) => (
|
||||||
|
<li class="mx-1 capitalize" role="listitem">
|
||||||
|
{index > 0 && <span class="inlin-block mr-1">/</span>}
|
||||||
|
{index !== parts.length - 1 ? (
|
||||||
|
<a class="text-txt-p dark:text-darkmode-txt-p" {...attrs}>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span class="text-txt-light dark:text-darkmode-txt-light">{label}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
link?: string;
|
||||||
|
newtab?: boolean;
|
||||||
|
type?: "primary" | "secondary";
|
||||||
|
color?: string;
|
||||||
|
hoverInvert?: boolean;
|
||||||
|
glass?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { label, link, newtab=false, type="primary", color, hoverInvert=false, glass=true }: Props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{ glass ? (
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target=`${newtab ? "_blank" : ""}`
|
||||||
|
class=`no-underline inline-block hover:opacity-100 ${type == "primary" ? "" : "intersect:animate-fade opacity-0"}`
|
||||||
|
>
|
||||||
|
<div class=`glass-t hover:bg-opacity-20 hover:backdrop-blur-none
|
||||||
|
${type == "primary" ? "px-4 py-2 rounded-md font-bold intersect:animate-fade opacity-0" : "px-2 py-0.5 rounded-sm text-txt-light dark:text-darkmode-txt-light"}`
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target=`${newtab ? "_blank" : ""}`
|
||||||
|
class=`no-underline inline-block group intersect:animate-fadeLeft opacity-0 ${hoverInvert ? "hover:opacity-100" : ""}`
|
||||||
|
>
|
||||||
|
<div class=`
|
||||||
|
${color ? color : "bg-bg-t dark:bg-darkmode-bg-t"}
|
||||||
|
${type == "primary" ? "p-0.5 rounded-md" : "p-[1.5px] rounded-sm"}`
|
||||||
|
>
|
||||||
|
<div class=`bg-bg-p dark:bg-darkmode-bg-p
|
||||||
|
${type == "primary" ? "px-4 py-2 rounded-md" : "px-2 py-0.5 rounded-sm"}
|
||||||
|
${hoverInvert ? "group-hover:bg-opacity-0" : ""}`
|
||||||
|
>
|
||||||
|
<div class=`text-transparent bg-clip-text
|
||||||
|
${color ? color : "bg-bg-t dark:bg-darkmode-bg-t"}
|
||||||
|
${type == "primary" ? "font-bold" : ""}
|
||||||
|
${hoverInvert ? "group-hover:text-txt-p dark:group-hover:text-darkmode-txt-p" : ""}`
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
---
|
||||||
|
import { formatDate } from "@lib/formatDate";
|
||||||
|
import readingTime from "@lib/readingTime";
|
||||||
|
import {
|
||||||
|
lowerHumanize,
|
||||||
|
upperHumanize,
|
||||||
|
markdownify,
|
||||||
|
slugify,
|
||||||
|
} from "@lib/textConverter";
|
||||||
|
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import {
|
||||||
|
FaRegCalendarAlt,
|
||||||
|
FaRegFolder,
|
||||||
|
FaRegUserCircle,
|
||||||
|
FaHashtag,
|
||||||
|
FaRegClock,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import type { GenericEntry, EntryReference } from "@/types";
|
||||||
|
|
||||||
|
interface EntryData {
|
||||||
|
title: string;
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
author?: EntryReference;
|
||||||
|
date?: string;
|
||||||
|
pubDate?: string;
|
||||||
|
modDate?: string;
|
||||||
|
categories?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
complexity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: GenericEntry;
|
||||||
|
showInfo?: boolean;
|
||||||
|
showImage?: boolean;
|
||||||
|
showTitle?: boolean;
|
||||||
|
showAuthor?: boolean;
|
||||||
|
showDate?: boolean;
|
||||||
|
showPubDate?: boolean;
|
||||||
|
showModDate?: boolean;
|
||||||
|
showReadingTime?: boolean;
|
||||||
|
showCategories?: boolean;
|
||||||
|
showTags?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
entry,
|
||||||
|
showInfo = true,
|
||||||
|
showImage = false,
|
||||||
|
showAuthor = false,
|
||||||
|
showDate = false,
|
||||||
|
showPubDate = false,
|
||||||
|
showModDate = false,
|
||||||
|
showReadingTime = false,
|
||||||
|
showCategories = false,
|
||||||
|
showTags = false,
|
||||||
|
}: Props = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
categories,
|
||||||
|
tags,
|
||||||
|
image,
|
||||||
|
imageAlt,
|
||||||
|
date,
|
||||||
|
pubDate,
|
||||||
|
modDate,
|
||||||
|
complexity,
|
||||||
|
} = entry.data as EntryData;
|
||||||
|
|
||||||
|
categories?.sort((a: string, b: string) => a.localeCompare(b));
|
||||||
|
tags?.sort((a: string, b: string) => a.localeCompare(b));
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
{
|
||||||
|
image && showImage && (
|
||||||
|
<div class="mb-8 glass rounded-lg intersect:animate-fadeDown opacity-0 intersect-no-queue">
|
||||||
|
<Image
|
||||||
|
class="w-full rounded-lg"
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt || ""}
|
||||||
|
height={500}
|
||||||
|
width={1200}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
showInfo && (
|
||||||
|
<div class="glass mb-8 p-4 rounded-lg intersect:animate-fade opacity-0 intersect-no-queue">
|
||||||
|
<h1 set:html={markdownify(title)} class="mb-4" />
|
||||||
|
<ul>
|
||||||
|
{author && showAuthor && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
<a href={`/authors/${slugify(author.id)}`}>
|
||||||
|
<FaRegUserCircle className={"-mt-1 inline-block"} />
|
||||||
|
{upperHumanize(author.id)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{date && showDate && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
<FaRegCalendarAlt className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{formatDate(date)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{pubDate && showPubDate && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
<FaRegCalendarAlt className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
Published
|
||||||
|
{formatDate(pubDate)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{modDate && showModDate && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
<FaRegCalendarAlt className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
Updated
|
||||||
|
{formatDate(modDate)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{complexity && complexity > 0 && showReadingTime && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
<FaRegClock className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{readingTime(entry.body!, complexity)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{categories && showCategories && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
{categories.map((category: string, index: number) => (
|
||||||
|
<a href={`/blog/categories/${slugify(category)}`}>
|
||||||
|
<FaRegFolder className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{upperHumanize(category)}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{tags && showTags && (
|
||||||
|
<li class="mr-1 inline-block">
|
||||||
|
{tags.map((tag: string, index: number) => (
|
||||||
|
<a href={`/blog/tags/${slugify(tag)}`}>
|
||||||
|
<FaHashtag className={"ml-2 -mt-1 inline-block"} />
|
||||||
|
{lowerHumanize(tag)}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
import Breadcrumbs from "@components/common/Breadcrumbs.astro";
|
||||||
|
import { upperHumanize } from "@lib/textConverter";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title = "" }: Props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="flex justify-center">
|
||||||
|
<div class="container text-center m-2">
|
||||||
|
<div class="rounded-lg p-0 bg-gradient-to-t gradient">
|
||||||
|
<div class="rounded-lg glass px-8 py-6 intersect:animate-fadeDown opacity-0 intersect-no-queue">
|
||||||
|
<h1 set:text={upperHumanize(title)} />
|
||||||
|
<Breadcrumbs className="mt-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
---
|
||||||
|
import { FaArrowLeft, FaArrowRight } from "react-icons/fa";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
collection?: string;
|
||||||
|
pageIndex?: number;
|
||||||
|
pageCount?: number;
|
||||||
|
pageIDs?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const { collection, pageIndex = 1, pageCount = 1, pageIDs } = Astro.props;
|
||||||
|
|
||||||
|
let pageList: number[] = [];
|
||||||
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
|
pageList.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevPage: string | null;
|
||||||
|
let nextPage: string | null;
|
||||||
|
|
||||||
|
if (pageIDs) {
|
||||||
|
prevPage =
|
||||||
|
pageIndex > 0
|
||||||
|
? `${collection ? "/" + collection : ""}/${pageIDs[pageIndex - 1]}`
|
||||||
|
: null;
|
||||||
|
nextPage =
|
||||||
|
pageIndex < pageCount - 1
|
||||||
|
? `${collection ? "/" + collection : ""}/${pageIDs[pageIndex + 1]}`
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
prevPage =
|
||||||
|
pageIndex === 2
|
||||||
|
? `${collection ? "/" + collection : "/"}`
|
||||||
|
: pageIndex > 2
|
||||||
|
? `${collection ? "/" + collection : ""}/page/${pageIndex - 1}`
|
||||||
|
: null;
|
||||||
|
nextPage =
|
||||||
|
pageIndex < pageCount
|
||||||
|
? `${collection ? "/" + collection : ""}/page/${pageIndex + 1}`
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
pageCount > 1 && (
|
||||||
|
<nav class="flex items-center justify-center pb-8">
|
||||||
|
<div class="row glass rounded-md p-2 m-2 space-x-2 intersect:animate-fadeUp opacity-0">
|
||||||
|
{/* previous */}
|
||||||
|
{prevPage ? (
|
||||||
|
<a
|
||||||
|
href={prevPage}
|
||||||
|
class="col rounded px-2 py-1.5 text-txt-p dark:text-darkmode-txt-p"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<FaArrowLeft className="inline-block" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="col rounded px-2 py-1.5 text-txt-light dark:text-darkmode-txt-light"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<FaArrowLeft className="inline-block" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* page index */}
|
||||||
|
{pageIDs ? (
|
||||||
|
<div
|
||||||
|
aria-current="page"
|
||||||
|
class="col rounded px-4 py-2 text-txt-p dark:text-darkmode-txt-p"
|
||||||
|
>
|
||||||
|
{pageIndex + 1}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
pageList.map((pagination) =>
|
||||||
|
pagination === pageIndex ? (
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
class="col rounded px-4 py-2 font-semibold text-txt-p dark:text-darkmode-txt-p border border-txt-light dark:border-darkmode-txt-light"
|
||||||
|
>
|
||||||
|
{pagination}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
pagination === 1
|
||||||
|
? `${collection ? "/" + collection : "/"}`
|
||||||
|
: `${collection ? "/" + collection : ""}/page/${pagination}`
|
||||||
|
}
|
||||||
|
aria-current="page"
|
||||||
|
class="col rounded px-4 py-2 font-semibold text-txt-p dark:text-darkmode-txt-p"
|
||||||
|
>
|
||||||
|
{pagination}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* next page */}
|
||||||
|
{nextPage ? (
|
||||||
|
<a
|
||||||
|
href={nextPage}
|
||||||
|
class="col rounded px-2 py-1.5 text-txt-p dark:text-darkmode-txt-p"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<FaArrowRight className="inline-block" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="col rounded px-2 py-1.5 text-txt-light dark:text-darkmode-txt-light"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<FaArrowRight className="inline-block" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
import {
|
||||||
|
IoMail,
|
||||||
|
IoLogoFacebook,
|
||||||
|
IoLogoPinterest,
|
||||||
|
IoLogoTwitter,
|
||||||
|
} from "react-icons/io5";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string | null;
|
||||||
|
folder?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, folder, id }: Props = Astro.props;
|
||||||
|
|
||||||
|
const baseURL = "https://janedoe.com";
|
||||||
|
|
||||||
|
const iconClass =
|
||||||
|
"glass-t rounded text-txt-p dark:text-darkmode-txt-p flex h-9 w-9 items-center justify-center text-center intersect:animate-fadeUp opacity-0 hover:bg-opacity-20";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul class="">
|
||||||
|
<li class="inline-block px-1">
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
href=`mailto:?subject=${title}&body=Check this out: ${baseURL}/${folder}/${id}%0D%0A%0D%0A${description}%0D%0A%0D%0A`
|
||||||
|
title="Share by Email"
|
||||||
|
aria-label="share by email"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<IoMail className="h-6 w-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="inline-block px-1">
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="facebook share button"
|
||||||
|
href={`https://facebook.com/sharer/sharer.php?u=${baseURL}/${folder}/${id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<IoLogoFacebook className="h-6 w-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="inline-block px-1">
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="twitter share button"
|
||||||
|
href={`https://twitter.com/intent/tweet/?text=${title}&url=${baseURL}/${folder}/${id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<IoLogoTwitter className="h-6 w-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="inline-block px-1">
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="pinterest share button"
|
||||||
|
href={`https://pinterest.com/pin/create/button/?url=${baseURL}/${folder}/${id}&media=&description=${description}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<IoLogoPinterest className="h-6 w-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
---
|
||||||
|
import type { SocialLinks } from "@/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FaDiscord,
|
||||||
|
FaEnvelope,
|
||||||
|
FaFacebook,
|
||||||
|
FaGithub,
|
||||||
|
FaInstagram,
|
||||||
|
FaLinkedin,
|
||||||
|
FaPinterest,
|
||||||
|
FaTiktok,
|
||||||
|
FaGlobe,
|
||||||
|
FaYoutube,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
links?: SocialLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { links } = Astro.props;
|
||||||
|
|
||||||
|
const liClass = "inline-block px-1 intersect:animate-fade opacity-0";
|
||||||
|
const iconClass =
|
||||||
|
"glass-t rounded text-txt-p dark:text-darkmode-txt-p flex h-9 w-9 items-center justify-center text-center";
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
links && (
|
||||||
|
<ul class="py-2 flex items-center">
|
||||||
|
{links.discord && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="Discord"
|
||||||
|
href={links.discord}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Discord</span>
|
||||||
|
<FaDiscord className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.email && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="Email"
|
||||||
|
href={`mailto:${links.email}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Email</span>
|
||||||
|
<FaEnvelope className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.facebook && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="Facebook"
|
||||||
|
href={links.facebook}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Facebook</span>
|
||||||
|
<FaFacebook className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.github && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="GitHub"
|
||||||
|
href={links.github}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">GitHub</span>
|
||||||
|
<FaGithub className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.instagram && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="Instagram"
|
||||||
|
href={links.instagram}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Instagram</span>
|
||||||
|
<FaInstagram className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.linkedIn && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="LinkedIn"
|
||||||
|
href={links.linkedIn}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">LinkedIn</span>
|
||||||
|
<FaLinkedin className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.pinterest && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="Pinterest"
|
||||||
|
href={links.pinterest}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Pinterest</span>
|
||||||
|
<FaPinterest className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.tiktok && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="TikTok"
|
||||||
|
href={links.tiktok}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">TikTok</span>
|
||||||
|
<FaTiktok className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.website && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="Website"
|
||||||
|
href={links.website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Website</span>
|
||||||
|
<FaGlobe className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{links.youtube && (
|
||||||
|
<li class={liClass}>
|
||||||
|
<a
|
||||||
|
class={iconClass}
|
||||||
|
aria-label="YouTube"
|
||||||
|
href={links.youtube}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<span class="sr-only">YouTube</span>
|
||||||
|
<FaYoutube className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
import type { HeadingHierarchy } from "@/types/index";
|
||||||
|
import type { MarkdownHeading } from "astro";
|
||||||
|
import { createHeadingHierarchy } from "@lib/tocUtils";
|
||||||
|
import { ScrollArea } from "@components/common/shortcodes/ScrollArea";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
headings: MarkdownHeading[];
|
||||||
|
tocDepth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headings, tocDepth = 3 } = Astro.props;
|
||||||
|
const heirarchy: HeadingHierarchy[] = createHeadingHierarchy(headings);
|
||||||
|
const toc = heirarchy.filter((heading) => heading.depth <= tocDepth);
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
toc.length > 0 && (
|
||||||
|
<div class="relative glass rounded-lg p-4 my-2 w-full intersect:animate-fadeLeft opacity-0">
|
||||||
|
<ScrollArea className="h-full" client:only="react">
|
||||||
|
<h2 class="text-2xl border-none">Contents</h2>
|
||||||
|
<ul class="list-none m-0">
|
||||||
|
{toc.map((heading) => (
|
||||||
|
<li class=`${heading.depth === 2 ? "ml-0" : "ml-4"}`>
|
||||||
|
<a href={`#${heading.slug}`} class=" w-full leading-none no-underline">
|
||||||
|
{heading.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
const Accordion = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 overflow-hidden rounded-lg border border-border dark:border-darkmode-border">
|
||||||
|
<button
|
||||||
|
className="glass-t-borderless flex w-full cursor-pointer items-center justify-between px-4 py-2 text-lg text-txt-p dark:text-darkmode-txt-p"
|
||||||
|
onClick={() => setShow(!show)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<svg
|
||||||
|
className={`ml-auto h-[.8em] w-[.8em] transition-transform duration-200 ${show ? "rotate-0" : "rotate-[-90deg]"}`}
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M505.755,123.592c-8.341-8.341-21.824-8.341-30.165,0L256.005,343.176L36.421,123.592c-8.341-8.341-21.824-8.341-30.165,0 s-8.341,21.824,0,30.165l234.667,234.667c4.16,4.16,9.621,6.251,15.083,6.251c5.462,0,10.923-2.091,15.083-6.251l234.667-234.667 C514.096,145.416,514.096,131.933,505.755,123.592z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={`max-h-0 overflow-hidden px-4 py-0 ${show && "max-h-full py-0"}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Accordion;
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { upperHumanize } from "@lib/textConverter";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function Notice({
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`mb-6 rounded-lg border border-current py-2 px-4
|
||||||
|
${type === "note" && "text-[#24a9ab]"}
|
||||||
|
${type === "tip" && "text-[#65ab24]"}
|
||||||
|
${type === "info" && "text-[#d7af2d]"}
|
||||||
|
${type === "warning" && "text-[#f43633]"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{type === "tip" ? (
|
||||||
|
<svg
|
||||||
|
className="mr-2"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0ZM12 2.4C6.69807 2.4 2.4 6.69807 2.4 12C2.4 17.3019 6.69807 21.6 12 21.6C17.3019 21.6 21.6 17.3019 21.6 12C21.6 6.69807 17.3019 2.4 12 2.4ZM15.9515 7.55147L9.6 13.9029L8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515C5.88284 12.8201 5.88284 13.5799 6.35147 14.0485L8.75147 16.4485C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485L17.6485 9.24853C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147C17.1799 7.08284 16.4201 7.08284 15.9515 7.55147Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : type === "info" ? (
|
||||||
|
<svg
|
||||||
|
className="mr-2"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.16109 0.993016C9.97971 1.03952 10.6611 1.42989 11.0721 2.22339L17.7981 15.8014C18.4502 17.1739 17.4403 19.0208 15.7832 19.0474H2.23859C0.730337 19.0234 -0.507163 17.3108 0.231587 15.7864L7.08321 2.20877C7.21146 1.96502 7.26996 1.89452 7.38059 1.76664C7.82534 1.25102 8.31171 0.975016 9.16109 0.993016ZM9.05046 2.49189C8.79284 2.50464 8.55696 2.64902 8.42834 2.87327C6.06134 7.36539 3.77946 11.9036 1.56546 16.4734C1.36071 16.9328 1.71209 17.5223 2.22621 17.547C6.74871 17.6201 11.2731 17.6201 15.7956 17.547C16.2925 17.523 16.666 16.953 16.459 16.4783C14.2866 11.9093 12.0471 7.37102 9.72171 2.87814C9.58446 2.63402 9.38309 2.48739 9.05046 2.49189Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9.61323 13.2153H8.35773L8.21973 7.04688H9.75723L9.61323 13.2153ZM8.17773 15.1015C8.17773 14.8731 8.25161 14.6841 8.39973 14.5338C8.54823 14.3838 8.75036 14.3084 9.00648 14.3084C9.26298 14.3084 9.46511 14.3838 9.61323 14.5338C9.76136 14.6841 9.83561 14.8731 9.83561 15.1015C9.83561 15.3216 9.76323 15.5057 9.61923 15.6539C9.47486 15.802 9.27086 15.8762 9.00648 15.8762C8.74211 15.8762 8.53811 15.802 8.39373 15.6539C8.24973 15.5057 8.17773 15.3216 8.17773 15.1015Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : type === "warning" ? (
|
||||||
|
<svg
|
||||||
|
className="mr-2"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 0C15.522 0 20 4.478 20 10C20 15.522 15.522 20 10 20C4.478 20 0 15.522 0 10C0 4.478 4.478 0 10 0ZM10 2C5.589 2 2 5.589 2 10C2 14.411 5.589 18 10 18C14.411 18 18 14.411 18 10C18 5.589 14.411 2 10 2ZM12.293 6.293L13.707 7.707L11.414 10L13.707 12.293L12.293 13.707L10 11.414L7.707 13.707L6.293 12.293L8.586 10L6.293 7.707L7.707 6.293L10 8.586L12.293 6.293Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className="mr-2"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<p className="font-secondary text-xl font-semibold text-txt-p dark:text-darkmode-txt-light my-0">
|
||||||
|
{upperHumanize(type)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notice;
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
|
|
||||||
|
// for shadcn components
|
||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
));
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="hidden relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
));
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function Tab({ name, children }: { name: string; children: React.ReactNode }) {
|
||||||
|
return <div data-name={name}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tab;
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { marked } from "marked";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
marked.use({
|
||||||
|
mangle: false,
|
||||||
|
headerIds: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Tabs = ({ children }: { children: React.ReactElement }) => {
|
||||||
|
const [active, setActive] = useState<number>(0);
|
||||||
|
const [defaultFocus, setDefaultFocus] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const tabRefs: React.RefObject<HTMLElement[]> = useRef([]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultFocus) {
|
||||||
|
//@ts-ignore
|
||||||
|
tabRefs.current[active]?.focus();
|
||||||
|
} else {
|
||||||
|
setDefaultFocus(true);
|
||||||
|
}
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
const tabLinks = Array.from(
|
||||||
|
children.props.value.matchAll(
|
||||||
|
/<div\s+data-name="([^"]+)"[^>]*>(.*?)<\/div>/gs,
|
||||||
|
),
|
||||||
|
(match: RegExpMatchArray) => ({ name: match[1], children: match[0] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = (
|
||||||
|
event: React.KeyboardEvent<EventTarget>,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
setActive(index);
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
setActive((active + 1) % tabLinks.length);
|
||||||
|
} else if (event.key === "ArrowLeft") {
|
||||||
|
setActive((active - 1 + tabLinks.length) % tabLinks.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-lg border border-border dark:border-darkmode-border">
|
||||||
|
<ul className="glass-t-borderless m-0 list-none flex border-b border-border dark:border-darkmode-border">
|
||||||
|
{tabLinks.map(
|
||||||
|
(item: { name: string; children: string }, index: number) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={`my-0 px-8 cursor-pointer border-b-[3px] pt-2 pb-1 text-lg text-txt-s dark:text-darkmode-txt-s border-border dark:border-darkmode-border
|
||||||
|
${index === active && "border-b-[3px] text-txt-p dark:text-darkmode-txt-p border-txt-p dark:border-darkmode-txt-p"}
|
||||||
|
`}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={index === active ? 0 : -1}
|
||||||
|
onKeyDown={(event) => handleKeyDown(event, index)}
|
||||||
|
onClick={() => setActive(index)}
|
||||||
|
//@ts-ignore
|
||||||
|
ref={(ref) => (tabRefs.current[index] = ref)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
{tabLinks.map((item: { name: string; children: string }, i: number) => (
|
||||||
|
<div
|
||||||
|
className={active === i ? "tab-content block px-5" : "hidden"}
|
||||||
|
key={i}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked.parse(item.children),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from "react";
|
||||||
|
import LiteYouTubeEmbed from "react-lite-youtube-embed";
|
||||||
|
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
|
||||||
|
|
||||||
|
const Youtube = ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<LiteYouTubeEmbed
|
||||||
|
wrapperClass="yt-lite rounded-lg"
|
||||||
|
id={id}
|
||||||
|
title={title}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Youtube;
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
import { ScrollArea } from "@components/common/shortcodes/ScrollArea";
|
||||||
|
import type { MenuItem } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menu: MenuItem[];
|
||||||
|
currentPage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { menu, currentPage } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
menu.length > 0 && (
|
||||||
|
<div class="relative glass rounded-lg p-4 my-2 w-full intersect:animate-fadeRight opacity-0">
|
||||||
|
<ScrollArea className="h-full" client:only="react">
|
||||||
|
<h2 class="text-2xl border-none">Documents</h2>
|
||||||
|
<ul class="list-none m-0">
|
||||||
|
{menu.map((item) => (
|
||||||
|
<li class="ml-0">
|
||||||
|
<a
|
||||||
|
href={`/docs/${item.id}`}
|
||||||
|
class={`w-full leading-none no-underline ${item.id === currentPage ? "font-bold" : ""}`}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</a>
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<ul class="list-none m-0">
|
||||||
|
{item.children.map((child) => (
|
||||||
|
<li class="ml-4">
|
||||||
|
<a
|
||||||
|
href={`/docs/${child.id}`}
|
||||||
|
class={`w-full leading-none no-underline ${child.id === currentPage ? "font-bold" : ""}`}
|
||||||
|
>
|
||||||
|
{child.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
import type { DocsEntry } from "@/types";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: DocsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const { title, image, imageAlt } = entry.data;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="h-full m-2 pl-0 bg-gradient-to-br gradient rounded-lg intersect:animate-fadeUp opacity-0"
|
||||||
|
>
|
||||||
|
<div class="glass h-full rounded-lg">
|
||||||
|
<a
|
||||||
|
class="group hover:opacity-100"
|
||||||
|
href={`/docs/${entry.id.replace("/-index", "")}`}
|
||||||
|
>
|
||||||
|
<div class="relative rounded-lg pb-9/16 overflow-hidden">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<Image
|
||||||
|
class="absolute object-cover w-full h-full group-hover:scale-[105%] transition-all duration-300"
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt}
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
class="glass-t absolute text-center rounded-lg px-2 py-1 top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%]"
|
||||||
|
>
|
||||||
|
<h4 class="">
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import DocCard from "@components/docs/Card.astro";
|
||||||
|
import PageHeader from "@components/common/PageHeader.astro";
|
||||||
|
import type { DocsEntry } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
collectionIndex: DocsEntry;
|
||||||
|
entries: DocsEntry[]; // this could either be the set of entries in a doc group, or the set of indices of the doc groups
|
||||||
|
}
|
||||||
|
|
||||||
|
const { collectionIndex, entries } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={collectionIndex.data.title}
|
||||||
|
image={collectionIndex.data.image?.src}
|
||||||
|
description={collectionIndex.data.description}
|
||||||
|
>
|
||||||
|
<PageHeader title={collectionIndex.data.title} />
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
{
|
||||||
|
entries.map((entry) => (
|
||||||
|
<div class="md:col-6 lg:col-4 h-min p-2">
|
||||||
|
<DocCard entry={entry} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "@components/base/BaseLayout.astro";
|
||||||
|
import type { DocsEntry, MenuItem } from "@/types";
|
||||||
|
import DocBrowser from "@components/docs/Browser.astro";
|
||||||
|
import TableOfContents from "@components/common/TableOfContents.astro";
|
||||||
|
import { render } from "astro:content";
|
||||||
|
import EntryHeader from "@components/common/EntryHeader.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: DocsEntry;
|
||||||
|
browserMenu: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry, browserMenu } = Astro.props;
|
||||||
|
const { Content, headings } = await render(entry);
|
||||||
|
const { title, description, hideNav, hideToc, image } = entry.data;
|
||||||
|
|
||||||
|
const globalHideNav = false; // set true to always hide the nav bar (left side menu)
|
||||||
|
const globalHideToc = false; // set true to always hide the table of contents (right side menu)
|
||||||
|
const tocDepth = 3; // maximum depth for the table of contents 1 = h1, etc
|
||||||
|
|
||||||
|
const actuallyHideNav = hideNav || globalHideNav;
|
||||||
|
const actuallyHideToc = hideToc || globalHideToc || headings.filter((heading) => heading.depth <= tocDepth).length === 0;
|
||||||
|
|
||||||
|
let articleClass = "";
|
||||||
|
let navClass = "";
|
||||||
|
let tocClass = "";
|
||||||
|
|
||||||
|
if (actuallyHideNav && actuallyHideToc) {
|
||||||
|
// Both nav and toc are hidden
|
||||||
|
navClass = "hidden";
|
||||||
|
articleClass = "w-full";
|
||||||
|
tocClass = "hidden";
|
||||||
|
} else if (!actuallyHideNav && actuallyHideToc) {
|
||||||
|
// Nav is visible, toc is hidden
|
||||||
|
navClass = "hidden md:flex md:w-1/4 lg:w-1/5";
|
||||||
|
articleClass = "w-full md:w-3/4 lg:w-4/5";
|
||||||
|
tocClass = "hidden";
|
||||||
|
} else if (actuallyHideNav && !actuallyHideToc) {
|
||||||
|
// Nav is hidden, toc is visible
|
||||||
|
navClass = "hidden";
|
||||||
|
articleClass = "w-full md:w-3/4 lg:w-4/5";
|
||||||
|
tocClass = "hidden md:flex md:w-1/4 lg:w-1/5";
|
||||||
|
} else {
|
||||||
|
// Both nav and toc are visble
|
||||||
|
navClass = "hidden md:flex md:w-1/4 lg:w-1/5";
|
||||||
|
articleClass = "w-full md:w-3/4 lg:w-3/5";
|
||||||
|
tocClass = "hidden lg:flex lg:w-1/5";
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={title} image={image?.src} description={description}>
|
||||||
|
<section class="flex container p-4">
|
||||||
|
<div class=`max-h-static_sidemenu sticky top-[5rem] mr-4 ${navClass}`>
|
||||||
|
<DocBrowser
|
||||||
|
menu={browserMenu}
|
||||||
|
currentPage={entry.id.replace("/-index", "")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class=`w-full ${articleClass}`>
|
||||||
|
<article class="">
|
||||||
|
<section class="">
|
||||||
|
<EntryHeader entry={entry} showImage showPubDate />
|
||||||
|
</section>
|
||||||
|
<section class="content mb-4 glass px-4 rounded-lg">
|
||||||
|
<Content />
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class=`max-h-static_sidemenu sticky top-[5rem] ml-4 ${tocClass}`>
|
||||||
|
<TableOfContents headings={headings} tocDepth={tocDepth} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||