Overhaul website completely (new theme)
|
|
@ -1,14 +0,0 @@
|
|||
; 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,4 @@
|
|||
// TODO: Follow https://www.tommyp.org/blog/adding-spotify-now-playing-to-astro and remove this comment
|
||||
CLIENT_ID=
|
||||
CLIENT_SECRET=
|
||||
REFRESH_TOKEN=
|
||||
|
|
@ -1,24 +1,52 @@
|
|||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
yarn.lock
|
||||
# package-lock.json
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
**/node_modules/**
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
|
@ -26,35 +54,85 @@ jspm_packages
|
|||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Environment variables
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# Cache
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
**/.cache
|
||||
.parcel-cache
|
||||
|
||||
# Additonal
|
||||
.vscode
|
||||
# Astro
|
||||
.astro
|
||||
.idea
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
.output/
|
||||
.json/
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Wrangler output
|
||||
.wrangler/
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for (testing) VSCode extensions
|
||||
.vscode
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Netlify
|
||||
.netlify
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"MD033": false,
|
||||
"MD013": false
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Dependency directories
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# Lock files
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Build and generated files
|
||||
*.min.*
|
||||
*.bundle.*
|
||||
*.map
|
||||
|
||||
# Git and version control
|
||||
**/.git
|
||||
|
||||
# Framework-specific files
|
||||
next-env.d.ts
|
||||
__generated__/
|
||||
|
||||
# Test and coverage files
|
||||
coverage/
|
||||
*.spec.*
|
||||
*.test.*
|
||||
|
||||
# Editor-specific files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Markdown and MDX files
|
||||
*.md
|
||||
*.mdx
|
||||
|
||||
# Changelog
|
||||
.changeset
|
||||
24
.prettierrc
|
|
@ -1,11 +1,29 @@
|
|||
{
|
||||
"plugins": ["prettier-plugin-astro"],
|
||||
"plugins": ["prettier-plugin-astro", "@trivago/prettier-plugin-sort-imports"],
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.astro"],
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"importOrder": [
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^@internal/(.*)$",
|
||||
"^@/models/(.*)$",
|
||||
"^@/utils/(.*)$",
|
||||
"^@/components/(?!ui/)(.*)$",
|
||||
"^@/components/ui/(.*)$",
|
||||
"^[./].*(?<!\\.(c|le|sc)ss)$",
|
||||
"^[.]/[-a-zA-Z0-9_]+[.](module)[.](css|scss|less)$"
|
||||
],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
|
|
|
|||
5
LICENSE
|
|
@ -1,7 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Reed Nelson
|
||||
Copyright (c) 2025 Kristian Takvam
|
||||
Copyright (c) 2024-present, trueberryless
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
19
README.md
|
|
@ -1,15 +1,16 @@
|
|||
<div align="center">
|
||||
<img src="src/assets/banner.svg" style="width:80%;" alt="banner" align="center" />
|
||||
</div>
|
||||
# Astro Portfolio Template @trueberryless
|
||||
|
||||
---
|
||||
[](https://astro.build)
|
||||
|
||||
[](https://github.com/Kremtastic/Kremtastic.github.io/blob/main/LICENSE) [](https://github.com/Kremtastic/Kremtastic.github.io)  [](https://kremtastic.github.io/)
|
||||
## Getting started
|
||||
|
||||
Heres the repo for my personal website!
|
||||
1. Use this template via the GitHub UI.
|
||||
2. Clone your own repo and open it in any editor of your choice (global search recommended).
|
||||
3. Search for `TODO` everywhere and complete them. Be aware: to fully complete these steps it will take you around one hour, so be patient, but I promise, it will be worth it 💜
|
||||
4. Deploy your website with Netlify, Vercel or in any other possible way, Astro supports: https://docs.astro.build/en/guides/deploy/
|
||||
|
||||
**Work in progress** - Please check in later. I promise it will be worth it.
|
||||
## License
|
||||
|
||||
Licensed under the MIT license, Copyright © trueberryless.
|
||||
|
||||
---
|
||||
###### *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)*
|
||||
See [LICENSE](/LICENSE) for more information.
|
||||
|
|
|
|||
|
|
@ -1,50 +1,17 @@
|
|||
import mdx from "@astrojs/mdx";
|
||||
// @ts-check
|
||||
import netlify from "@astrojs/netlify";
|
||||
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 { defineConfig, envField } from "astro/config";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://kremtastic.github.io",
|
||||
base: "/",
|
||||
trailingSlash: "ignore",
|
||||
prefetch: {
|
||||
prefetchAll: true
|
||||
},
|
||||
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
|
||||
},
|
||||
|
||||
// Stop Vite from SSR/prebundling CommonJS packages
|
||||
vite: {
|
||||
ssr: {
|
||||
noExternal: ["react-lite-youtube-embed"], // prevent SSR import
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["react-lite-youtube-embed"], // don't prebundle it
|
||||
integrations: [react()],
|
||||
adapter: netlify(),
|
||||
env: {
|
||||
schema: {
|
||||
CLIENT_ID: envField.string({ context: "server", access: "secret" }),
|
||||
CLIENT_SECRET: envField.string({ context: "server", access: "secret" }),
|
||||
REFRESH_TOKEN: envField.string({ context: "server", access: "secret" }),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
# 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`.
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# 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.
|
||||
81
package.json
|
|
@ -1,68 +1,35 @@
|
|||
{
|
||||
"name": "astrogon",
|
||||
"version": "1.0.0",
|
||||
"description": "Template.",
|
||||
"author": "Jane Doe",
|
||||
"name": "trueberryless",
|
||||
"homepage": "https://github.com/trueberryless-org/trueberryless",
|
||||
"bugs": {
|
||||
"url": "https://github.com/trueberryless-org/trueberryless/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/trueberryless-org/trueberryless.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"packageManager": "npm@10.8.2",
|
||||
"author": "trueberryless <trueberryless@gmail.com> (https://trueberryless.org)",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"postbuild": "pagefind --site dist",
|
||||
"format": "prettier -w ./src"
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"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-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"
|
||||
"@astrojs/netlify": "^6.5.11",
|
||||
"@astrojs/react": "^4.4.0",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"astro-webrings": "^0.1.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"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",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||
"astro": "^5.14.5",
|
||||
"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"
|
||||
}
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-astro": "0.14.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
_worker.js
|
||||
_routes.json
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
##### 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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
https://kremtastic.github.io/
|
||||
|
After Width: | Height: | Size: 442 KiB |
|
|
@ -1,9 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -1,6 +0,0 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Disallow: /api/*
|
||||
|
||||
Sitemap: https://kremtastic.github.io/sitemap-index.xml
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 1024 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1024 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- Main large crystal -->
|
||||
<g transform="translate(50,45)">
|
||||
<!-- Large crystal body -->
|
||||
<path d="M0,-30 L15,0 L0,20 L-15,0 Z"
|
||||
fill="#FF3333"
|
||||
stroke="#CC0000"
|
||||
stroke-width="1"/>
|
||||
|
||||
<!-- Inner highlight -->
|
||||
<path d="M0,-25 L10,-5 L0,15 L-10,-5 Z"
|
||||
fill="#FF6666"/>
|
||||
</g>
|
||||
|
||||
<!-- Left smaller crystal -->
|
||||
<g transform="translate(35,70) rotate(-15)">
|
||||
<path d="M0,-15 L8,0 L0,10 L-8,0 Z"
|
||||
fill="#FF3333"
|
||||
stroke="#CC0000"
|
||||
stroke-width="1"/>
|
||||
<!-- Inner highlight -->
|
||||
<path d="M0,-12 L5,-2 L0,7 L-5,-2 Z"
|
||||
fill="#FF6666"/>
|
||||
</g>
|
||||
|
||||
<!-- Right smaller crystal -->
|
||||
<g transform="translate(65,70) rotate(15)">
|
||||
<path d="M0,-15 L8,0 L0,10 L-8,0 Z"
|
||||
fill="#FF3333"
|
||||
stroke="#CC0000"
|
||||
stroke-width="1"/>
|
||||
<!-- Inner highlight -->
|
||||
<path d="M0,-12 L5,-2 L0,7 L-5,-2 Z"
|
||||
fill="#FF6666"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
import profilePicture from "../assets/profile-picture.png";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const { name, subtitle } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="intro">
|
||||
<div class="content">
|
||||
<img src={profilePicture.src} alt="trueberryless" class="pfp" />
|
||||
<div class="text">
|
||||
<h1>Yo, I'm {name}!</h1>
|
||||
<p class="subtitle">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.intro {
|
||||
padding: 8rem 2rem 6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.pfp {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--ctp-lavender);
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.pfp:hover {
|
||||
transform: scale(1.05);
|
||||
border-color: var(--ctp-pink);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
color: var(--ctp-subtext0);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.intro {
|
||||
padding: 4rem 2rem 3rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pfp {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
---
|
||||
// TODO: List some projects
|
||||
const projects = [
|
||||
{
|
||||
name: "Portal Room",
|
||||
url: "https://github.com/Kremtastic/PortalRoom",
|
||||
color: "var(--ctp-lavender)",
|
||||
},
|
||||
{
|
||||
name: "Raytracer",
|
||||
url: "https://github.com/Kremtastic/RaytracerUnity",
|
||||
color: "var(--ctp-peach)",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section class="project">
|
||||
<div class="content">
|
||||
<h2 class="section-title">Projects</h2>
|
||||
<div class="projects">
|
||||
{
|
||||
projects.map((project) => (
|
||||
<a
|
||||
href={project.url}
|
||||
class="project-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={`--project-color: ${project.color}`}
|
||||
>
|
||||
<span class="project-icon">◈</span>
|
||||
<span class="project-name">{project.name}</span>
|
||||
<svg class="project-arrow" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M7 17L17 7M17 7H7M17 7V17"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.project {
|
||||
padding: 6rem 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-text);
|
||||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.projects {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 2rem;
|
||||
background-color: var(--ctp-surface0);
|
||||
border: 2px solid var(--project-color);
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--ctp-text);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-link::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background-color: var(--project-color);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.project-link:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.project-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--project-color);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
flex: 1;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
z-index: 1;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.project-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--project-color);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-link:hover {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.project-link:hover .project-icon,
|
||||
.project-link:hover .project-arrow {
|
||||
color: var(--ctp-base);
|
||||
}
|
||||
|
||||
.project-link:hover .project-name {
|
||||
color: var(--ctp-base);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.project {
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
// TODO: Adapt social links
|
||||
const socials = [
|
||||
{
|
||||
name: "GitHub",
|
||||
url: "https://github.com/Kremtastic",
|
||||
color: "var(--ctp-sapphire)",
|
||||
icon: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>`,
|
||||
},
|
||||
{
|
||||
name: "Bluesky",
|
||||
url: "https://bsky.app/profile/kremtastic.bsky.social",
|
||||
color: "var(--ctp-sky)",
|
||||
icon: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z"/></svg>`,
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section class="socials">
|
||||
<div class="content">
|
||||
<div class="social-links">
|
||||
{
|
||||
socials.map((social) => (
|
||||
<a
|
||||
href={social.url}
|
||||
class="social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={`--social-color: ${social.color}`}
|
||||
aria-label={social.name}
|
||||
>
|
||||
<span class="social-icon" set:html={social.icon} />
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.socials {
|
||||
padding: 4rem 2rem 8rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--ctp-surface0);
|
||||
border: 2px solid var(--social-color);
|
||||
border-radius: 50%;
|
||||
color: var(--social-color);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.social-icon :global(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
background-color: var(--social-color);
|
||||
color: var(--ctp-base);
|
||||
transform: translateY(-4px) rotate(5deg);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.social-link:active {
|
||||
transform: translateY(-2px) rotate(5deg);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.socials {
|
||||
padding: 3rem 2rem 6rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
const SoundWave = ({ progress, isPlaying }) => {
|
||||
const [animationTimings, setAnimationTimings] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const timings = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
timings.push(Math.floor(Math.random() * 800) + 700);
|
||||
}
|
||||
setAnimationTimings(timings);
|
||||
}, []);
|
||||
|
||||
const createLines = () => {
|
||||
const progressNum = Math.floor(progress / 2);
|
||||
const lines = [];
|
||||
const y1 = isPlaying ? 3 : 8;
|
||||
const y2 = isPlaying ? 25 : 20;
|
||||
|
||||
for (let i = 1; i < 51; i++) {
|
||||
const isFull = i <= progressNum;
|
||||
lines.push(
|
||||
<line
|
||||
key={i}
|
||||
x1={i + i * 6}
|
||||
y1={y1}
|
||||
x2={i + i * 6}
|
||||
y2={y2}
|
||||
strokeWidth="2"
|
||||
stroke={isFull ? "var(--ctp-green)" : "var(--ctp-overlay0)"}
|
||||
style={{
|
||||
transformOrigin: "center",
|
||||
transition: "stroke 1s ease-in-out",
|
||||
animation: isPlaying
|
||||
? `sound ${animationTimings[i]}ms linear infinite alternate`
|
||||
: "none",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<svg
|
||||
width="302"
|
||||
height="28"
|
||||
viewBox="0 0 344 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ maxWidth: "100%", height: "auto" }}
|
||||
>
|
||||
{createLines()}
|
||||
</svg>
|
||||
<style>{`
|
||||
@keyframes sound {
|
||||
0% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoundWave;
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import SpotifyPlayer from "./SpotifyPlayer";
|
||||
---
|
||||
|
||||
<section class="spotify">
|
||||
<div class="content">
|
||||
<div class="spotify-container">
|
||||
<SpotifyPlayer client:only="react" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.spotify {
|
||||
padding: 6rem 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spotify-container {
|
||||
background-color: var(--ctp-surface0);
|
||||
border: 2px solid var(--ctp-green);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.spotify {
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
|
||||
.spotify-container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import SoundWave from "./SoundWave";
|
||||
|
||||
const SpotifyPlayer = () => {
|
||||
const [track, setTrack] = useState(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNowPlaying = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/spotify/now-playing");
|
||||
if (response.status === 204) {
|
||||
setTrack(null);
|
||||
setIsPlaying(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setTrack(data);
|
||||
setIsPlaying(data.isPlaying);
|
||||
setProgress(data.progress || 0);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching now playing:", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNowPlaying();
|
||||
const interval = setInterval(fetchNowPlaying, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: "center", color: "var(--ctp-subtext0)" }}>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!track) {
|
||||
return (
|
||||
<div style={{ textAlign: "center", color: "var(--ctp-subtext0)" }}>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
style={{ margin: "0 auto 1rem", color: "var(--ctp-green)" }}
|
||||
>
|
||||
<path
|
||||
d="M9 18V5l12-2v13M9 18c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm12-2c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<p>Not playing anything right now</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||
<div style={{ display: "flex", gap: "1.5rem", alignItems: "center" }}>
|
||||
{track.albumArt && (
|
||||
<img
|
||||
src={track.albumArt}
|
||||
alt={track.album}
|
||||
style={{
|
||||
width: "80px",
|
||||
height: "80px",
|
||||
borderRadius: "8px",
|
||||
border: "2px solid var(--ctp-green)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "600",
|
||||
color: "var(--ctp-text)",
|
||||
marginBottom: "0.25rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{track.title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
color: "var(--ctp-subtext0)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{track.artist}
|
||||
</div>
|
||||
{track.album && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
color: "var(--ctp-subtext1)",
|
||||
marginTop: "0.25rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{track.album}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<SoundWave progress={progress} isPlaying={isPlaying} />
|
||||
</div>
|
||||
|
||||
{track.trackUrl && (
|
||||
<a
|
||||
href={track.trackUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.75rem 1.5rem",
|
||||
backgroundColor: "var(--ctp-green)",
|
||||
color: "var(--ctp-base)",
|
||||
borderRadius: "9999px",
|
||||
textDecoration: "none",
|
||||
fontWeight: "500",
|
||||
alignSelf: "center",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1.05)";
|
||||
e.currentTarget.style.backgroundColor = "var(--ctp-teal)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
e.currentTarget.style.backgroundColor = "var(--ctp-green)";
|
||||
}}
|
||||
>
|
||||
Open in Spotify
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpotifyPlayer;
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
interface Props {
|
||||
color: string;
|
||||
speed?: number; // Duration in seconds, default 60
|
||||
size?: number; // Circle size multiplier, default 1
|
||||
direction?: "left" | "right"; // Animation direction, default 'left'
|
||||
}
|
||||
|
||||
const { color, speed = 60, size = 1, direction = "left" } = Astro.props;
|
||||
|
||||
const colorMap: any = {
|
||||
rosewater: "var(--ctp-rosewater)",
|
||||
flamingo: "var(--ctp-flamingo)",
|
||||
pink: "var(--ctp-pink)",
|
||||
mauve: "var(--ctp-mauve)",
|
||||
red: "var(--ctp-red)",
|
||||
maroon: "var(--ctp-maroon)",
|
||||
peach: "var(--ctp-peach)",
|
||||
yellow: "var(--ctp-yellow)",
|
||||
green: "var(--ctp-green)",
|
||||
teal: "var(--ctp-teal)",
|
||||
sky: "var(--ctp-sky)",
|
||||
sapphire: "var(--ctp-sapphire)",
|
||||
blue: "var(--ctp-blue)",
|
||||
lavender: "var(--ctp-lavender)",
|
||||
};
|
||||
|
||||
const waveColor = colorMap[color];
|
||||
|
||||
// Generate smooth wave pattern
|
||||
const generateWavePath = () => {
|
||||
let path = "M 0,20";
|
||||
const circleWidth = 10 * size;
|
||||
const amplitude = 12 * size;
|
||||
const circles = 120;
|
||||
|
||||
for (let i = 0; i < circles; i++) {
|
||||
const x = i * circleWidth;
|
||||
const direction = i % 2 === 0 ? -1 : 1;
|
||||
const y = 20 + amplitude * direction;
|
||||
path += ` Q ${x + circleWidth / 2},${y} ${x + circleWidth},20`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
const wavePath = generateWavePath();
|
||||
const animationClass =
|
||||
direction === "left" ? "wave-slide-left" : "wave-slide-right";
|
||||
---
|
||||
|
||||
<div class="wave-container">
|
||||
<div class="wave-wrapper" style={`--wave-speed: ${speed}s`}>
|
||||
<svg
|
||||
class="wave"
|
||||
class:list={[animationClass]}
|
||||
viewBox="0 0 600 40"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path
|
||||
class="wave-path"
|
||||
d={wavePath}
|
||||
stroke={waveColor}
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wave-container {
|
||||
width: 100%;
|
||||
padding: 3rem 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wave-wrapper {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
black 8%,
|
||||
black 92%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
black 8%,
|
||||
black 92%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.wave {
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
.wave-path {
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.wave-slide-left {
|
||||
animation: wave-slide-left var(--wave-speed) linear infinite;
|
||||
}
|
||||
|
||||
.wave-slide-right {
|
||||
animation: wave-slide-right var(--wave-speed) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wave-slide-left {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-33.33%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave-slide-right {
|
||||
0% {
|
||||
transform: translateX(-33.33%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.wave-container {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
---
|
||||
// TODO: Adjust the webrings accoring to https://astro-webrings.lou.gg/ if you want.
|
||||
const webrings = [
|
||||
{
|
||||
name: "Join the Astro webring (shoutouts to Lou)",
|
||||
url: "https://astro-webrings.lou.gg/",
|
||||
color: "var(--ctp-red)",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section class="webring">
|
||||
<div class="content">
|
||||
<h2 class="section-title">Webrings</h2>
|
||||
<div class="rings">
|
||||
{
|
||||
webrings.map((ring) => (
|
||||
<a
|
||||
href={ring.url}
|
||||
class="ring-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={`--ring-color: ${ring.color}`}
|
||||
>
|
||||
<span class="ring-icon">◈</span>
|
||||
<span class="ring-name">{ring.name}</span>
|
||||
<svg class="ring-arrow" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M7 17L17 7M17 7H7M17 7V17"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.webring {
|
||||
padding: 6rem 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-text);
|
||||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.ring-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 2rem;
|
||||
background-color: var(--ctp-surface0);
|
||||
border: 2px solid var(--ring-color);
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--ctp-text);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ring-link::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background-color: var(--ring-color);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ring-link:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.ring-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--ring-color);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ring-name {
|
||||
flex: 1;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
z-index: 1;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.ring-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--ring-color);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ring-link:hover {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.ring-link:hover .ring-icon,
|
||||
.ring-link:hover .ring-arrow {
|
||||
color: var(--ctp-base);
|
||||
}
|
||||
|
||||
.ring-link:hover .ring-name {
|
||||
color: var(--ctp-base);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.webring {
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.ring-link {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.ring-name {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
<!-- ---
|
||||
// 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>
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
---
|
||||
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 = "Kristian Takvam";
|
||||
const siteDescription = "Kristian Takvam's website.";
|
||||
const siteImage = "@assets/og-image.png";
|
||||
const siteAuthor = "Kristian Takvam";
|
||||
const baseUrl = "https://kremtastic.github.io";
|
||||
---
|
||||
|
||||
<!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>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
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",
|
||||
rss: "/rss.xml",
|
||||
//instagram: "https://www.instagram.com/",
|
||||
linkedIn: "https://www.linkedin.com/in/kristian-emil-takvam-70a475215/",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<!-- 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>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
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 />
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
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>
|
||||