weeee💃
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
# Ignore Astro files
|
||||
*.astro
|
||||
|
||||
# Ignore node_modules directory
|
||||
node_modules/
|
||||
|
||||
# Ignore build output
|
||||
dist/
|
124
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at [satnaingdev@gmail.com](satnaingdev@gmail.com).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at [this link](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the [FAQ](https://www.contributor-covenant.org/faq). [Translations](https://www.contributor-covenant.org/translations) are also available.
|
55
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
# How to contribute to AstroPaper
|
||||
|
||||
Thank you for your interest in contributing to **AstroPaper**! We appreciate every contribution, whether you're fixing a typo, improving documentation, or adding a new feature.
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
There are several ways to contribute to **AstroPaper**, and every contribution counts\_ whether it's a PR for a major feature or a small fix.
|
||||
|
||||
You can also contribute by leaving review comments on PRs, adding ideas to existing GitHub Issues and Discussions, or helping others by answering questions in GitHub Discussions.
|
||||
|
||||
Here’s a summary of the different ways you can contribute:
|
||||
|
||||
- [Opening a new issue](#open-a-new-issue)
|
||||
- [Submitting PRs](#feature-requests)
|
||||
- [Solving an existing issue](#solving-an-issue)
|
||||
- [Making changes to a blog post](#making-changes-to-a-blog-post)
|
||||
- [Helping others by answering issues/discussions](#helping-with-github-issuesdiscussions)
|
||||
- [Reviewing existing PRs](#reviewing-existing-prs)
|
||||
- [Starting a discussion](#starting-a-discussion)
|
||||
|
||||
## Open a new Issue
|
||||
|
||||
If you find a bug or problem, first check whether a similar issue already exists. If you don’t find any open issue that addresses the bug/problem you’re facing, feel free to [open a new issue](https://github.com/satnaing/astro-paper/issues/new/choose).
|
||||
|
||||
## Feature Requests
|
||||
|
||||
If you have an idea for a new feature or enhancement that could improve AstroPaper, we’d love to hear it! Before submitting a new feature request, please:
|
||||
|
||||
1. **Check existing discussions/issues**: Review the [Discussions](https://github.com/satnaing/astro-paper/discussions) or [Issues](https://github.com/satnaing/astro-paper/issues) to see if the feature has already been requested or discussed. You can contribute by adding your thoughts or upvoting existing requests.
|
||||
2. **Open a new issue**: If you don’t find an existing discussion, you can open a new issue using the [Feature Request Template](https://github.com/satnaing/astro-paper/issues/new?assignees=&labels=enhancement&projects=&template=%E2%9C%A8-feature-request.md&title=%5BFeature+Request%5D%3A+). Be as detailed as possible, describing the problem this feature would solve and how it would benefit AstroPaper users.
|
||||
3. **Discuss first**: If you’re unsure whether your idea is feasible or fits the project’s goals, feel free to [start a GitHub Discussion](https://github.com/satnaing/astro-paper/discussions/new/choose) to gather feedback from the community.
|
||||
|
||||
## Making PRs (Pull Requests)
|
||||
|
||||
### Solving an Issue
|
||||
|
||||
Browse through the existing issues to find one that interests you. You can use labels to filter the issues. See the [Label](https://github.com/satnaing/astro-paper/labels) section for more information.
|
||||
|
||||
### Making Changes to a Blog Post
|
||||
|
||||
For small changes like typos, syntax fixes, or broken links, click the "Suggest Changes" link below the title of any blog post. This will take you to the .md file, where you can make your changes and submit a pull request for review. For more significant changes to a blog post, it’s recommended to open a new issue or discussion first.
|
||||
|
||||
## Helping with GitHub Issues/Discussions
|
||||
|
||||
GitHub Discussions and Issues are great places to help others. Whether you're a long-time user of AstroPaper or just have experience with a specific problem, we encourage you to answer questions or solve issues when possible.
|
||||
|
||||
## Reviewing Existing PRs
|
||||
|
||||
You can help by reviewing and providing feedback on open PRs. Different perspectives can be very helpful.
|
||||
|
||||
Since AstroPaper doesn’t currently have automated testing, it’s especially useful if you can do manual testing on open PRs and provide feedback.
|
||||
|
||||
## Starting a Discussion
|
||||
|
||||
If you’re unsure whether your issue warrants a fix or if you just want to share ideas and get feedback, feel free to [start a GitHub discussion](https://github.com/satnaing/astro-paper/discussions/new/choose). It’s a great way to engage with the community.
|
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
github: [satnaing]
|
||||
buy_me_a_coffee: satnaing
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: AstroPaper Discussions
|
||||
url: https://github.com/satnaing/astro-paper/discussions
|
||||
about: Please ask and answer questions here.
|
19
.github/ISSUE_TEMPLATE/✨-feature-request.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: "✨ Feature Request"
|
||||
about: Suggest an idea for improving AstroPaper
|
||||
title: "[Feature Request]: "
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
27
.github/ISSUE_TEMPLATE/🐞-bug-report.md
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "\U0001F41E Bug report"
|
||||
about: Report a bug or unexpected behavior in AstroPaper
|
||||
title: "[BUG]: "
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
16
.github/ISSUE_TEMPLATE/📝-documentation-improvement.md
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: "\U0001F4DD Documentation Improvement"
|
||||
about: Propose updates or improvements to the documentation/blog posts
|
||||
title: "[Docs]: "
|
||||
labels: documentation
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the Issue**
|
||||
A clear and concise description of the documentation issue or improvement.
|
||||
|
||||
**Proposed Changes**
|
||||
Describe what changes should be made and why they would improve the documentation.
|
||||
|
||||
**Additional Context**
|
||||
Add any other context or screenshots about the documentation request here.
|
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
## Description
|
||||
|
||||
<!-- A clear and concise description of what the pull request does. Include any relevant motivation and background. -->
|
||||
|
||||
## Types of changes
|
||||
|
||||
<!-- What types of changes does your code introduce to AstroPaper? Put an `x` in the boxes that apply -->
|
||||
|
||||
- [ ] Bug Fix (non-breaking change which fixes an issue)
|
||||
- [ ] New Feature (non-breaking change which adds functionality)
|
||||
- [ ] Documentation Update (if none of the other choices apply)
|
||||
- [ ] Others (any other types not listed above)
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Please follow this checklist and put an x in each of the boxes, like this: [x]. You can also fill these out after creating the PR. This is simply a reminder of what we are going to look for before merging your code. -->
|
||||
|
||||
- [ ] I have read the [Contributing Guide](https://github.com/satnaing/astro-paper/blob/main/.github/CONTRIBUTING.md)
|
||||
- [ ] I have added the necessary documentation (if appropriate)
|
||||
- [ ] Breaking Change (fix or feature that would cause existing functionality to not work as expected)
|
||||
|
||||
## Further comments
|
||||
|
||||
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!-- If this PR is related to an existing issue, link to it here. -->
|
||||
|
||||
Closes: #<!-- Issue number, if applicable -->
|
42
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- reopened
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Code standards & build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: "☁️ Checkout repository"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "🔧 Setup Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
|
||||
- name: "📦 Install dependencies"
|
||||
run: npm ci
|
||||
|
||||
- name: "🔎 Lint code"
|
||||
run: npm run lint
|
||||
|
||||
- name: "📝 Checking code format"
|
||||
run: npm run format:check
|
||||
|
||||
- name: "🚀 Build the project"
|
||||
run: npm run build
|
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# build output
|
||||
dist/
|
||||
.output/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# ignore .astro directory
|
||||
.astro
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
4
.markdownlint.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"MD033": false,
|
||||
"MD013": false
|
||||
}
|
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
# Expose Astro dependencies for `pnpm` users
|
||||
shamefully-hoist=true
|
13
.prettierignore
Normal file
@ -0,0 +1,13 @@
|
||||
# Ignore everything
|
||||
/*
|
||||
|
||||
# Except these files & folders
|
||||
!/src
|
||||
!/public
|
||||
!/.github
|
||||
!tsconfig.json
|
||||
!astro.config.ts
|
||||
!package.json
|
||||
!.prettierrc
|
||||
!eslint.config.mjs
|
||||
!README.md
|
20
.prettierrc
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
34
.vscode/astro-paper.code-snippets
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"Frontmatter": {
|
||||
"scope": "markdown",
|
||||
"prefix": "frontmatter",
|
||||
"body": [
|
||||
"---",
|
||||
"author: $1",
|
||||
"pubDatetime: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}T$CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND.000$CURRENT_TIMEZONE_OFFSET",
|
||||
"modDatetime: $3",
|
||||
"title: $4",
|
||||
"featured: ${5|false,true|}",
|
||||
"draft: ${6|true,false|}",
|
||||
"tags:",
|
||||
" - $7",
|
||||
"description: $8",
|
||||
"---",
|
||||
],
|
||||
"description": "Adds the frontmatter block for the AstroPaper Blog post"
|
||||
},
|
||||
"Blog Template": {
|
||||
"scope": "markdown",
|
||||
"prefix": "template",
|
||||
"body": [
|
||||
"${1:frontmatter}",
|
||||
"",
|
||||
"${2: Introductory Sentence}",
|
||||
"",
|
||||
"## Table of contents",
|
||||
"",
|
||||
"## ${3: heading 1}",
|
||||
],
|
||||
"description": "Adds the template for the AstroPaper Blog post. You will need to trigger the snippet modal on the 'frontmatter' line to insert the other snipper."
|
||||
}
|
||||
}
|
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
159
AstroPaper-lighthouse-score.svg
Normal file
@ -0,0 +1,159 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="theme--agnostic" fill="none" width="1000" height="330">
|
||||
<style>
|
||||
.gauge-base {
|
||||
opacity: 0.1
|
||||
}
|
||||
|
||||
.gauge-arc {
|
||||
fill: none;
|
||||
animation-delay: 250ms;
|
||||
stroke-linecap: round;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 100px 60px;
|
||||
animation: load-gauge 1s ease forwards
|
||||
}
|
||||
|
||||
.guage-text {
|
||||
font-size: 40px;
|
||||
font-family: monospace;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.guage-red {
|
||||
color: #ff4e42;
|
||||
fill: #ff4e42;
|
||||
stroke: #ff4e42
|
||||
}
|
||||
.guage-orange {
|
||||
color: #ffa400;
|
||||
fill: #ffa400;
|
||||
stroke: #ffa400
|
||||
}
|
||||
.guage-green {
|
||||
color: #0cce6b;
|
||||
fill: #0cce6b;
|
||||
stroke: #0cce6b
|
||||
}
|
||||
.theme--agnostic .guage-undefined {
|
||||
color: #5c5c5c;
|
||||
fill: #5c5c5c;
|
||||
stroke: #5c5c5c
|
||||
}
|
||||
.theme--light .guage-undefined {
|
||||
color: #1e1e1e;
|
||||
fill: #1e1e1e;
|
||||
stroke: #1e1e1e
|
||||
}
|
||||
.theme--dark .guage-undefined {
|
||||
color: #f5f5f5;
|
||||
fill: #f5f5f5;
|
||||
stroke: #f5f5f5
|
||||
}
|
||||
|
||||
.guage-title {
|
||||
stroke: none;
|
||||
font-size: 26px;
|
||||
line-height: 26px;
|
||||
font-family: Roboto, Halvetica, Arial, sans-serif
|
||||
}
|
||||
.metric.guage-title {
|
||||
font-family: 'Courier New', Courier, monospace
|
||||
}
|
||||
.theme--agnostic .guage-title {
|
||||
color: #737373;
|
||||
fill: #737373
|
||||
}
|
||||
.theme--light .guage-title {
|
||||
color: #212121;
|
||||
fill: #212121
|
||||
}
|
||||
.theme--dark .guage-title {
|
||||
color: #f5f5f5;
|
||||
fill: #f5f5f5
|
||||
}
|
||||
|
||||
@keyframes load-gauge {
|
||||
from {
|
||||
stroke-dasharray: 0 352.858
|
||||
}
|
||||
}
|
||||
.lh-gauge--pwa__disc {
|
||||
fill: #e0e0e0
|
||||
}
|
||||
.lh-gauge--pwa__logo {
|
||||
position: relative;
|
||||
fill: #b0b0b0
|
||||
}
|
||||
.lh-gauge--pwa__invisible {
|
||||
display: none
|
||||
}
|
||||
.lh-gauge--pwa__visible {
|
||||
display: inline
|
||||
}
|
||||
.guage-invisible {
|
||||
display: none
|
||||
}
|
||||
.lh-gauge--pwa__logo--primary-color {
|
||||
fill: #304ffe
|
||||
}
|
||||
.theme--agnostic .lh-gauge--pwa__logo--secondary-color {
|
||||
fill: #787878
|
||||
}
|
||||
.theme--light .lh-gauge--pwa__logo--secondary-color {
|
||||
fill: #3d3d3d
|
||||
}
|
||||
.theme--dark .lh-gauge--pwa__logo--secondary-color {
|
||||
fill: #d8b6b6
|
||||
}
|
||||
.theme--light #svg_2 {
|
||||
stroke: #00000022
|
||||
}
|
||||
.theme--agnostic #svg_2 {
|
||||
stroke: #616161
|
||||
}
|
||||
.theme--light #svg_2 {
|
||||
stroke: #00000022
|
||||
}
|
||||
.theme--dark #svg_2 {
|
||||
stroke: #f5f5f566
|
||||
}
|
||||
</style>
|
||||
<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="100" y="0">
|
||||
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
|
||||
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
|
||||
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
|
||||
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">Performance</text>
|
||||
</svg>,<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="300" y="0">
|
||||
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
|
||||
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
|
||||
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
|
||||
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">Accessibility</text>
|
||||
</svg>,<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="500" y="0">
|
||||
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
|
||||
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
|
||||
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
|
||||
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">Best Practices</text>
|
||||
</svg>,<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="700" y="0">
|
||||
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
|
||||
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
|
||||
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
|
||||
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">SEO</text>
|
||||
</svg>
|
||||
<svg width="604" height="76" x="200" y="250">
|
||||
<g>
|
||||
<rect fill="none" id="canvas_background" height="80" width="604" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<rect fill-opacity="0" stroke-width="2" rx="40" id="svg_2" height="72" width="600" y="1" x="0" fill="#000000"/>
|
||||
<rect stroke="#000" rx="8" id="svg_3" height="14" width="48" y="30" x="35" stroke-opacity="null" stroke-width="0" fill="#ff4e42"/>
|
||||
<rect stroke="#000" rx="6" id="svg_4" height="14" width="48" y="30" x="220" stroke-opacity="null" stroke-width="0" fill="#ffa400"/>
|
||||
<rect stroke="#000" rx="6" id="svg_5" height="14" width="48" y="30" x="410" stroke-opacity="null" stroke-width="0" fill="#0cce6b"/>
|
||||
<text class="metric guage-title" xml:space="preserve" text-anchor="start" font-size="26" id="svg_6" y="45" x="100" stroke-opacity="null" stroke-width="0" stroke="#000">0-49</text>
|
||||
<text class="metric guage-title" xml:space="preserve" text-anchor="start" font-size="26" id="svg_7" y="45" x="280" stroke-opacity="null" stroke-width="0" stroke="#000">50-89</text>
|
||||
<text class="metric guage-title" xml:space="preserve" text-anchor="start" font-size="26" id="svg_8" y="45" x="470" stroke-opacity="null" stroke-width="0" stroke="#000">90-100</text>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
# Base stage for building the static files
|
||||
FROM node:lts AS base
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage for serving the application
|
||||
FROM nginx:mainline-alpine-slim AS runtime
|
||||
COPY --from=base ./app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sat Naing
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
187
README.md
Normal file
@ -0,0 +1,187 @@
|
||||
# AstroPaper 📄
|
||||
|
||||

|
||||
[](https://www.figma.com/community/file/1356898632249991861)
|
||||

|
||||

|
||||
[](https://conventionalcommits.org)
|
||||
[](http://commitizen.github.io/cz-cli/)
|
||||
|
||||
AstroPaper is a minimal, responsive, accessible and SEO-friendly Astro blog theme. This theme is designed and crafted based on [my personal blog](https://satnaing.dev/blog).
|
||||
|
||||
This theme follows best practices and provides accessibility out of the box. Light and dark mode are supported by default. Moreover, additional color schemes can also be configured.
|
||||
|
||||
This theme is self-documented \_ which means articles/posts in this theme can also be considered as documentations. Read [the blog posts](https://astro-paper.pages.dev/posts/) or check [the README Documentation Section](#-documentation) for more info.
|
||||
|
||||
## 🔥 Features
|
||||
|
||||
- [x] type-safe markdown
|
||||
- [x] super fast performance
|
||||
- [x] accessible (Keyboard/VoiceOver)
|
||||
- [x] responsive (mobile ~ desktops)
|
||||
- [x] SEO-friendly
|
||||
- [x] light & dark mode
|
||||
- [x] fuzzy search
|
||||
- [x] draft posts & pagination
|
||||
- [x] sitemap & rss feed
|
||||
- [x] followed best practices
|
||||
- [x] highly customizable
|
||||
- [x] dynamic OG image generation for blog posts [#15](https://github.com/satnaing/astro-paper/pull/15) ([Blog Post](https://astro-paper.pages.dev/posts/dynamic-og-image-generation-in-astropaper-blog-posts/))
|
||||
|
||||
_Note: I've tested screen-reader accessibility of AstroPaper using **VoiceOver** on Mac and **TalkBack** on Android. I couldn't test all other screen-readers out there. However, accessibility enhancements in AstroPaper should be working fine on others as well._
|
||||
|
||||
## ✅ Lighthouse Score
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pagespeed.web.dev/report?url=https%3A%2F%2Fastro-paper.pages.dev%2F&form_factor=desktop">
|
||||
<img width="710" alt="AstroPaper Lighthouse Score" src="AstroPaper-lighthouse-score.svg">
|
||||
<a>
|
||||
</p>
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of AstroPaper, you'll see the following folders and files:
|
||||
|
||||
```bash
|
||||
/
|
||||
├── public/
|
||||
│ ├── assets/
|
||||
│ │ └── logo.svg
|
||||
│ │ └── logo.png
|
||||
│ └── favicon.svg
|
||||
│ └── astropaper-og.jpg
|
||||
│ └── robots.txt
|
||||
│ └── toggle-theme.js
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ │ └── socialIcons.ts
|
||||
│ ├── components/
|
||||
│ ├── content/
|
||||
│ │ | blog/
|
||||
│ │ | └── some-blog-posts.md
|
||||
│ │ └── config.ts
|
||||
│ ├── layouts/
|
||||
│ └── pages/
|
||||
│ └── styles/
|
||||
│ └── utils/
|
||||
│ └── config.ts
|
||||
│ └── types.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
All blog posts are stored in `src/content/blog` directory.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Documentation can be read in two formats\_ _markdown_ & _blog post_.
|
||||
|
||||
- Configuration - [markdown](src/content/blog/how-to-configure-astropaper-theme.md) | [blog post](https://astro-paper.pages.dev/posts/how-to-configure-astropaper-theme/)
|
||||
- Add Posts - [markdown](src/content/blog/adding-new-post.md) | [blog post](https://astro-paper.pages.dev/posts/adding-new-posts-in-astropaper-theme/)
|
||||
- Customize Color Schemes - [markdown](src/content/blog/customizing-astropaper-theme-color-schemes.md) | [blog post](https://astro-paper.pages.dev/posts/customizing-astropaper-theme-color-schemes/)
|
||||
- Predefined Color Schemes - [markdown](src/content/blog/predefined-color-schemes.md) | [blog post](https://astro-paper.pages.dev/posts/predefined-color-schemes/)
|
||||
|
||||
> For AstroPaper v1, check out [this branch](https://github.com/satnaing/astro-paper/tree/astro-paper-v1) and this [live URL](https://astro-paper-v1.astro-paper.pages.dev/)
|
||||
|
||||
## 💻 Tech Stack
|
||||
|
||||
**Main Framework** - [Astro](https://astro.build/)
|
||||
**Type Checking** - [TypeScript](https://www.typescriptlang.org/)
|
||||
**Component Framework** - [ReactJS](https://reactjs.org/)
|
||||
**Styling** - [TailwindCSS](https://tailwindcss.com/)
|
||||
**UI/UX** - [Figma Design File](https://www.figma.com/community/file/1356898632249991861)
|
||||
**Fuzzy Search** - [FuseJS](https://fusejs.io/)
|
||||
**Icons** - [Boxicons](https://boxicons.com/) | [Tablers](https://tabler-icons.io/)
|
||||
**Code Formatting** - [Prettier](https://prettier.io/)
|
||||
**Deployment** - [Cloudflare Pages](https://pages.cloudflare.com/)
|
||||
**Illustration in About Page** - [https://freesvgillustration.com](https://freesvgillustration.com/)
|
||||
**Linting** - [ESLint](https://eslint.org)
|
||||
|
||||
## 👨🏻💻 Running Locally
|
||||
|
||||
You can start using this project locally by running the following command in your desired directory:
|
||||
|
||||
```bash
|
||||
# npm 6.x
|
||||
npm create astro@latest --template satnaing/astro-paper
|
||||
|
||||
# npm 7+, extra double-dash is needed:
|
||||
npm create astro@latest -- --template satnaing/astro-paper
|
||||
|
||||
# yarn
|
||||
yarn create astro --template satnaing/astro-paper
|
||||
|
||||
# pnpm
|
||||
pnpm dlx create-astro --template satnaing/astro-paper
|
||||
```
|
||||
|
||||
> **_Warning!_** If you're using `yarn 1`, you might need to [install `sharp`](https://sharp.pixelplumbing.com/install) as a dependency.
|
||||
|
||||
Then start the project by running the following commands:
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
npm run install
|
||||
|
||||
# start running the project
|
||||
npm run dev
|
||||
```
|
||||
|
||||
As an alternative approach, if you have Docker installed, you can use Docker to run this project locally. Here's how:
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker build -t astropaper .
|
||||
|
||||
# Run the Docker container
|
||||
docker run -p 4321:80 astropaper
|
||||
```
|
||||
|
||||
## Google Site Verification (optional)
|
||||
|
||||
You can easily add your [Google Site Verification HTML tag](https://support.google.com/webmasters/answer/9008080#meta_tag_verification&zippy=%2Chtml-tag) in AstroPaper using an environment variable. This step is optional. If you don't add the following environment variable, the google-site-verification tag won't appear in the HTML `<head>` section.
|
||||
|
||||
```bash
|
||||
# in your environment variable file (.env)
|
||||
PUBLIC_GOOGLE_SITE_VERIFICATION=your-google-site-verification-value
|
||||
```
|
||||
|
||||
> See [this discussion](https://github.com/satnaing/astro-paper/discussions/334#discussioncomment-10139247) for adding AstroPaper to the Google Search Console.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
> **_Note!_** For `Docker` commands we must have it [installed](https://docs.docker.com/engine/install/) in your machine.
|
||||
|
||||
| Command | Action |
|
||||
| :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run format:check` | Check code format with Prettier |
|
||||
| `npm run format` | Format codes with Prettier |
|
||||
| `npm run sync` | Generates TypeScript types for all Astro modules. [Learn more](https://docs.astro.build/en/reference/cli-reference/#astro-sync). |
|
||||
| `npm run lint` | Lint with ESLint |
|
||||
| `docker compose up -d` | Run AstroPaper on docker, You can access with the same hostname and port informed on `dev` command. |
|
||||
| `docker compose run app npm install` | You can run any command above into the docker container. |
|
||||
| `docker build -t astropaper .` | Build Docker image for AstroPaper. |
|
||||
| `docker run -p 4321:80 astropaper` | Run AstroPaper on Docker. The website will be accessible at `http://localhost:4321`. |
|
||||
|
||||
> **_Warning!_** Windows PowerShell users may need to install the [concurrently package](https://www.npmjs.com/package/concurrently) if they want to [run diagnostics](https://docs.astro.build/en/reference/cli-reference/#astro-check) during development (`astro check --watch & astro dev`). For more info, see [this issue](https://github.com/satnaing/astro-paper/issues/113).
|
||||
|
||||
## ✨ Feedback & Suggestions
|
||||
|
||||
If you have any suggestions/feedback, you can contact me via [my email](mailto:contact@satnaing.dev). Alternatively, feel free to open an issue if you find bugs or want to request new features.
|
||||
|
||||
## 📜 License
|
||||
|
||||
Licensed under the MIT License, Copyright © 2023
|
||||
|
||||
---
|
||||
|
||||
Made with 🤍 by [Sat Naing](https://satnaing.dev) 👨🏻💻 and [contributors](https://github.com/satnaing/astro-paper/graphs/contributors).
|
46
astro.config.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import react from "@astrojs/react";
|
||||
import remarkToc from "remark-toc";
|
||||
import remarkCollapse from "remark-collapse";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import { SITE } from "./src/config";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: SITE.website,
|
||||
integrations: [
|
||||
tailwind({
|
||||
applyBaseStyles: false,
|
||||
}),
|
||||
react(),
|
||||
sitemap({
|
||||
filter: page => SITE.showArchives || !page.endsWith("/archives"),
|
||||
}),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
remarkToc,
|
||||
[
|
||||
remarkCollapse,
|
||||
{
|
||||
test: "Table of contents",
|
||||
},
|
||||
],
|
||||
],
|
||||
shikiConfig: {
|
||||
// For more themes, visit https://shiki.style/themes
|
||||
themes: { light: "min-light", dark: "night-owl" },
|
||||
wrap: true,
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
exclude: ["@resvg/resvg-js"],
|
||||
},
|
||||
},
|
||||
scopedStyleStrategy: "where",
|
||||
experimental: {
|
||||
contentLayer: true,
|
||||
},
|
||||
});
|
7
cz.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
commitizen:
|
||||
name: cz_conventional_commits
|
||||
tag_format: v$version
|
||||
update_changelog_on_bump: true
|
||||
version_provider: npm
|
||||
version_scheme: semver
|
11
docker-compose.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: node:20
|
||||
ports:
|
||||
- 4321:4321
|
||||
working_dir: /app
|
||||
command: npm run dev -- --host 0.0.0.0
|
||||
volumes:
|
||||
- ./:/app
|
44
eslint.config.mjs
Normal file
@ -0,0 +1,44 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import astroParser from "astro-eslint-parser";
|
||||
import eslintPluginAstro from "eslint-plugin-astro";
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...eslintPluginAstro.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.astro"],
|
||||
languageOptions: {
|
||||
parser: astroParser,
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".astro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["tailwind.config.cjs", "**/*.d.ts"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/triple-slash-reference": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**", ".astro"],
|
||||
},
|
||||
];
|
9723
package-lock.json
generated
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "blog",
|
||||
"version": "4.7.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"sync": "astro sync",
|
||||
"astro": "astro",
|
||||
"format:check": "prettier --check . --plugin=prettier-plugin-astro",
|
||||
"format": "prettier --write . --plugin=prettier-plugin-astro",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"astro": "^4.16.18",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"remark-collapse": "^0.1.2",
|
||||
"remark-toc": "^9.0.0",
|
||||
"satori": "^0.11.0",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^3.6.2",
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/lodash.kebabcase": "^4.1.9",
|
||||
"@types/react": "^18.3.6",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"astro-eslint-parser": "^1.0.3",
|
||||
"eslint": "^9.10.0",
|
||||
"eslint-plugin-astro": "^1.2.4",
|
||||
"globals": "^15.9.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript-eslint": "^8.5.0"
|
||||
}
|
||||
}
|
361
public/assets/dev.svg
Normal file
@ -0,0 +1,361 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="865.76" height="682.89" viewBox="0 0 865.76 682.89">
|
||||
|
||||
<defs>
|
||||
|
||||
<style xmlns="http://www.w3.org/1999/xhtml">*, body, html { -webkit-font-smoothing: antialiased; }
|
||||
img, svg { max-width: 100%; }
|
||||
</style>
|
||||
|
||||
</defs>
|
||||
|
||||
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#fff" opacity="0.7"/>
|
||||
|
||||
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#fff" opacity="0.7"/>
|
||||
|
||||
<rect x="104.67" y="206.46" width="463.2" height="348.88" fill="#fff"/>
|
||||
|
||||
<rect x="108.43" y="206.46" width="459.44" height="35.42" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="128.82" y="259.06" width="104.13" height="104.13" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="713.86" y="369.62" width="5.37" height="37.57" fill="#999"/>
|
||||
|
||||
<polygon points="664.89 442.18 664.89 554.44 672.53 554.44 676.93 436.58 664.89 442.18" fill="#ccc"/>
|
||||
|
||||
<polygon points="711.71 420.08 711.71 537.08 719.36 537.08 723.52 414.71 711.71 420.08" fill="#ccc"/>
|
||||
|
||||
<polygon points="668.23 434.1 733.18 405.05 703.86 399.96 670.01 385.44 668.23 434.1" fill="#ccc"/>
|
||||
|
||||
<path d="M656.14,446.25l77-35.83v-5.37L668.23,434.1S660.68,442.36,656.14,446.25Z" fill="#b3b3b3"/>
|
||||
|
||||
<path d="M693.46,271.94H734a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H693.46a0,0,0,0,1,0,0V271.94A0,0,0,0,1,693.46,271.94Z" fill="#999"/>
|
||||
|
||||
<rect x="241.54" y="44.36" width="325.8" height="139.55" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="263.01" y="83.01" width="100.91" height="65.48" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M297.36,131.59a1.07,1.07,0,0,1-.76-.32l-14.79-14.76a1.08,1.08,0,0,1,0-1.5l14.79-15.56a1.07,1.07,0,0,1,1.56,1.47l-14.07,14.81,14.05,14a1.07,1.07,0,0,1,0,1.52A1.09,1.09,0,0,1,297.36,131.59Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M328.73,132.66a1.06,1.06,0,0,1-.76-.31,1.07,1.07,0,0,1,0-1.52l14-14L328,102a1.08,1.08,0,1,1,1.56-1.48l14.78,15.56a1.06,1.06,0,0,1,0,1.5l-14.78,14.77A1.07,1.07,0,0,1,328.73,132.66Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M305.56,131.59a1.08,1.08,0,0,1-1-1.56l14.34-28.18a1.08,1.08,0,1,1,1.92,1L306.51,131A1.07,1.07,0,0,1,305.56,131.59Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<path d="M524.39,119.51H454.62a1.08,1.08,0,0,1,0-2.15h69.77a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M540.5,132.39H454.62a1.08,1.08,0,0,1,0-2.15H540.5a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<rect x="460.52" y="153.86" width="65.48" height="16.1" rx="7.5" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M567.33,44.36V183.91H241.54s54.75-59.1,144.51-74c4.1-.68,8.24-1.12,12.38-1.4C426.41,106.6,557.79,95.18,567.33,44.36Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="31.14" y="128.09" width="187.86" height="213.62" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="31.14" y="128.09" width="187.86" height="34.35" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="46.17" y="173.18" width="57.97" height="57.97" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<circle cx="164.78" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<circle cx="184.11" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<circle cx="203.43" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M170.69,192.5H117a1.07,1.07,0,1,1,0-2.14h53.67a1.07,1.07,0,0,1,0,2.14Z" fill="#fff"/>
|
||||
|
||||
<path d="M186.25,205.38h-68.7a1.07,1.07,0,0,1,0-2.14h68.7a1.07,1.07,0,1,1,0,2.14Z" fill="#fff"/>
|
||||
|
||||
<path d="M203.43,218.27H117.55a1.08,1.08,0,0,1,0-2.15h85.88a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M168,287H84.28a1.08,1.08,0,1,1,0-2.15H168a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M194.84,299.85H57.44a1.08,1.08,0,1,1,0-2.15h137.4a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M168.54,312.73H83.74a1.08,1.08,0,1,1,0-2.15h84.8a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<rect x="83.74" y="248.32" width="78.36" height="16.1" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="256.57" y="259.06" width="66.55" height="17.18" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M308.78,293.79H256.57a1.08,1.08,0,1,1,0-2.15h52.21a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M325.8,306.67H256.57a1.07,1.07,0,1,1,0-2.14H325.8a1.07,1.07,0,1,1,0,2.14Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M339.76,319.55H256.57a1.07,1.07,0,1,1,0-2.14h83.19a1.07,1.07,0,0,1,0,2.14Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M379.48,332.44H256.57a1.08,1.08,0,1,1,0-2.15H379.48a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="256.57" y="348.15" width="154.58" height="15.03" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M252.45,400.29h-122a1.08,1.08,0,0,1,0-2.15h122a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M353.18,400.29H268.91a1.08,1.08,0,0,1,0-2.15h84.27a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M417.59,400.29H388.06a1.08,1.08,0,0,1,0-2.15h29.53a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="256.57" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="360.69" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="373.57" y="396.53" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M223.29,429.16H131a1.08,1.08,0,0,1,0-2.15h92.32a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M289.84,455.37H129.9a1.08,1.08,0,1,1,0-2.15H289.84a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M325.27,429.16H255a1.08,1.08,0,1,1,0-2.15h70.31a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M349.42,455.37h-36a1.08,1.08,0,0,1,0-2.15h36a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="227.58" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="240.46" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="290.92" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="303.8" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M355.32,512.93H298.43a1.08,1.08,0,0,1,0-2.15h56.89a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M416,512.93H388.06a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="361.77" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="374.65" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M416,455.37H375.72a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="353.18" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="366.06" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M205,485H131a1.08,1.08,0,0,1,0-2.15H205a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M349.42,485h-52.6a1.08,1.08,0,0,1,0-2.15h52.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M416,485H363.38a1.08,1.08,0,1,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="207.19" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="220.07" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="231.88" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M256.57,512.93H131a1.08,1.08,0,0,1,0-2.15h125.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="258.71" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="271.59" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="283.4" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="244.76" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="259.79" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="271.59" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="284.48" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M417.59,429.16H358a1.08,1.08,0,1,1,0-2.15h59.58a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="330.63" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="343.52" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="51.53" y="436.18" width="103.05" height="64.41" fill="#787878" data-primary="true"/>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M88.5,485.36a1.06,1.06,0,0,1-.74-.3l-15.5-14.83a1.06,1.06,0,0,1,0-1.54l15.49-15a1.07,1.07,0,0,1,1.52,0,1.08,1.08,0,0,1,0,1.52l-14.7,14.25,14.69,14.06a1.07,1.07,0,0,1,0,1.52A1.1,1.1,0,0,1,88.5,485.36Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M119.16,485.36a1.07,1.07,0,0,1-.74-1.84l14.69-14.26L118.42,455.2a1.07,1.07,0,0,1,1.48-1.55l15.5,14.83a1.07,1.07,0,0,1,.33.77,1.08,1.08,0,0,1-.32.78l-15.5,15A1.08,1.08,0,0,1,119.16,485.36Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M96.62,483.41a1.11,1.11,0,0,1-.5-.12,1.07,1.07,0,0,1-.45-1.45l14-26.83a1.08,1.08,0,1,1,1.91,1l-14,26.83A1.06,1.06,0,0,1,96.62,483.41Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<rect x="434.76" y="367.48" width="11.81" height="208.25" fill="#999"/>
|
||||
|
||||
<rect x="441.2" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
|
||||
|
||||
<rect x="471.26" y="368.01" width="11.81" height="172.29" fill="#999"/>
|
||||
|
||||
<rect x="477.7" y="368.01" width="5.37" height="172.29" opacity="0.1"/>
|
||||
|
||||
<rect x="728.89" y="367.48" width="11.81" height="208.25" fill="#999"/>
|
||||
|
||||
<rect x="735.33" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
|
||||
|
||||
<rect x="758.95" y="354.06" width="11.81" height="186.25" fill="#999"/>
|
||||
|
||||
<rect x="765.39" y="354.06" width="5.37" height="186.25" opacity="0.1"/>
|
||||
|
||||
<path d="M688.1,271.94h40.53a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H688.1a0,0,0,0,1,0,0V271.94A0,0,0,0,1,688.1,271.94Z" fill="#b3b3b3"/>
|
||||
|
||||
<polygon points="421.88 364.26 477.27 336.37 786.88 336.37 750.36 364.26 421.88 364.26" fill="#ccc"/>
|
||||
|
||||
<path d="M542.11,559.63l-32.5,25.42S496,597.2,507.76,604.71c0,0,17.17,10.74,31.13-7.51l19.37-31.64Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M505.61,596.12c8,8.68,20.58,6.87,28.45-1,3.7-3.79,7-8.33,10.52-12.3,3.08-3.62,7.51-8.79,10.65-12.28-2.8,3.74-7.06,9.09-10,12.81-3.41,4.12-6.73,8.65-10.42,12.54-8.21,8.11-21.45,9.88-29.19.26Z" opacity="0.2"/>
|
||||
|
||||
<path d="M512.32,583.74c6.45-.09,13.31,2.42,17.35,7.63a15.61,15.61,0,0,1,2.79,5.84c-.26-.47-.51-1-.74-1.43a8.51,8.51,0,0,0-.81-1.37c-4-6.39-11.44-9.4-18.59-10.67Z" opacity="0.2"/>
|
||||
|
||||
<path d="M519.56,580c4.83-.65,11.72.93,12.9,6.4-2.62-4.61-8.1-5.41-12.9-6.4Z" opacity="0.2"/>
|
||||
|
||||
<path d="M523.86,575.73c4.82-.65,11.72.93,12.89,6.39-2.61-4.6-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
|
||||
|
||||
<path d="M532.45,569.29c4.82-.65,11.72.93,12.89,6.39-2.61-4.61-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
|
||||
|
||||
<path d="M550.16,544.06l-8,15.57s-3.32,4,1.25,6.48a8.52,8.52,0,0,0,4.06,1h7.9a3.61,3.61,0,0,0,2.94-1.51L568,551.93S554.41,546.7,550.16,544.06Z" fill="#f9b499"/>
|
||||
|
||||
<polygon points="548.32 510.23 551.84 520.86 557.18 505.66 548.32 510.23" fill="#f9b499"/>
|
||||
|
||||
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1.24.24,0,0,1-.08.06,11.71,11.71,0,0,1-3.82,1.75h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68,6.38,6.38,0,0,0-3.07,0l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48,86.2,86.2,0,0,1,8.82-12.47c.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13a10.63,10.63,0,0,1,3.59,2.59c6.62,6.85,11.81,23.17,11.81,23.17l14,46.16A30.89,30.89,0,0,1,710.77,332.4Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M675.8,305s-30.74,5-53.75.22c-.59-.12-1.17-.27-1.75-.43A88.92,88.92,0,0,0,592.56,302l-22.06-.18-1.09,7.3h36.87s12,.39,21.7,3.61c0,0,9.66,3.22,29,1.07l21.82-2.66Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M683.8,255.21c-20.39,2.6-56.89,14.58-56.89,14.58-8.59-6.44-35.49-12.47-35.49-12.47.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13A10.63,10.63,0,0,1,683.8,255.21Z" opacity="0.2"/>
|
||||
|
||||
<path d="M620.1,254.32a12.38,12.38,0,0,1-1.24.26c-7.26,1.28-14.75-1.87-20.74-8a43,43,0,0,1-10.73-19.86c-4.59-18.58,2.63-36.33,16.12-39.66s28.13,9,32.72,27.59S633.6,251,620.1,254.32Z" fill="#f9b499"/>
|
||||
|
||||
<ellipse cx="639.26" cy="215.05" rx="1.61" ry="3.22" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
|
||||
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a15.44,15.44,0,0,1-.54-3.65,15.8,15.8,0,0,1,.54-4.54l-8.59-4.36s-4.67.85-7.17-4.52c0,0-8.93,0-7.86-6.44,0,0-6.44,4.3-8.59-1.07,0,0-9.69,6.88-19.33-4.62,0,0-4.28,7.84-3,21,0,0-6.34,5.08-9.93.38a6.63,6.63,0,0,1-1.28-3.77,5.58,5.58,0,0,1,3.22-5.49s-7.77-2.89-7.56-9.28a10.2,10.2,0,0,1,1.41-4.67s1.61-4,7.63-2.31h0a19.17,19.17,0,0,1,3.1,1.24s-8.21-17.26,3.4-28.49c0,0,19.14-19.82,26.66,4.87,0,0,6.55-10.14,17-7.62h0a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M590.41,197.6s-3.22-10.46,6.44-16.91c0,0,6.93-4.51,16.49,1.46a23.89,23.89,0,0,1,2.73,2.07,16.44,16.44,0,0,0,10.59,4.11s11-.47,12.6,12c0,0-12.35-10.72-21.47-5.54,0,0-4.83-15.22-17.72-10.93C600.07,183.91,592.56,186.06,590.41,197.6Z" opacity="0.2"/>
|
||||
|
||||
<path d="M579.79,195.56c-5.23.93-9,7-9,7a10.2,10.2,0,0,1,1.41-4.67S573.77,193.84,579.79,195.56Z" opacity="0.2"/>
|
||||
|
||||
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a7.4,7.4,0,0,1-.54-3.65,7.26,7.26,0,0,1,.54-2.07l4.29-6.83h0c3.22-1.93,3.22-6.66,3.22-6.66a10.45,10.45,0,0,0,4.63-3.63,8.74,8.74,0,0,0,1.09-8.24c-2.23-5.68-8.94-4.09-8.94-4.09,2.15-16-7.52-10.85-7.52-10.85-1-14.52-7.22-17.15-7.61-17.3a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" opacity="0.2"/>
|
||||
|
||||
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87-5.79-9c20.4-4.65,18.67-31.41,18.67-31.41h3.34a17.63,17.63,0,0,0,2.14,11.2c3.91-1.57,9-1.42,11.4-1.23a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f9b499"/>
|
||||
|
||||
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87c11.69-5.09,23-18.83,23-18.83,4.06-6-2.64-9.53-4.26-10.28a.16.16,0,0,1,0-.29c3.86-1.42,8.68-1.27,11-1.09a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f7a48b"/>
|
||||
|
||||
<path d="M618.86,254.58l3.07,4.81s18.66-10.53,15-26.36C637,233,635.5,251.18,618.86,254.58Z" fill="#f7a48b"/>
|
||||
|
||||
<path d="M599,253.69a55.57,55.57,0,0,1,18.79,6.51" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
|
||||
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1c.36-.24,1.68-1.46-.08-5l-5.63-8.42a1.13,1.13,0,0,1,.39-1.6,1.07,1.07,0,0,1,.55-.14,1.12,1.12,0,0,1,.91.46l7.14,9.93s1.07,4.29,15-2.15C696.69,340.64,708.75,336,710.77,332.4Z" opacity="0.2"/>
|
||||
|
||||
<path d="M674.48,349.38h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68s8.22-3.51,12.51,7.22C674.14,341.71,675.46,346.35,674.48,349.38Z" opacity="0.2"/>
|
||||
|
||||
<path d="M677.36,323.46s-14,5.89-18.8,11l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48c.3,0,8.89,2.15,17.47,32.21,0,0,8.53,31.81,22,30.4h23.5s21.08.73,21.08-13.23V297.7s.65-8.42,4.09-3.13L681.66,317S683.8,321.32,677.36,323.46Z" opacity="0.2"/>
|
||||
|
||||
<path d="M680.58,258c-8.42,6.71-12.77,17.28-12.88,27.91-.1-1.33-.27-2.68-.25-4,0-9.45,4.89-19.05,13.13-23.89Z" opacity="0.2"/>
|
||||
|
||||
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<g opacity="0.2">
|
||||
|
||||
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z"/>
|
||||
|
||||
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z"/>
|
||||
|
||||
</g>
|
||||
|
||||
<path d="M706.34,371.77c0,.17,0,8.76-2.14,25.76l-.34,2.43a58.67,58.67,0,0,0-.52,7.27c-.06,3.84-2.56,11.15-21.3,8.21.49-.83.94-1.65,1.36-2.49l.35-.68c1.57-3,3.69-7.58,4.35-11.52,0,0-59-5.36-78.37-12.88,0,0-28.3-11.81-38.84-8.05l-8.39,7s-7.51-2.15,3.22-15Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M688.1,400.75s13.8.92,13.29,7a4,4,0,0,1-1.61,2.85c-1.84,1.41-6.25,3.29-16.15,1.89A63,63,0,0,0,688.1,400.75Z" opacity="0.2"/>
|
||||
|
||||
<path d="M564.14,385.44s-6.33-4,6.75-5.62Z" opacity="0.2"/>
|
||||
|
||||
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46-19.1,2.58-45.12-11.68-45.12-11.68l14-39.72c11.81-44,23.61-56.89,23.61-56.89l26.54-55.12,1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46,20-25.72,31.27-83,31.27-83-.89-13.33,38.47-47.86,38.47-47.86,15-15-4.29-19.33-4.29-19.33-17.27-2.72-39.92-10.84-46.46-13.25l1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" opacity="0.2"/>
|
||||
|
||||
<path d="M589.37,430.17l-33.31,13.52,13,26s-5.54,16.19-11.4,35.76c0,0-13.42,9.12-27.37,8.05,0,0-11.81-30.06-18.25-61.19,0,0-8.59-18.25,6.44-30.06L562,387.05l8.93-7.23s6.64-4.83,38.84,8.05Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" opacity="0.2"/>
|
||||
|
||||
<path d="M589.37,430.17l-33.31,13.52,13,26a370.36,370.36,0,0,1-11.89,36s-12.93,8.88-26.88,7.81c39.72-3.22,20.78-67.22,20.78-67.22-4.38-9.78,2.19-12.65,6.18-13.48a53.87,53.87,0,0,0,7.5-2.12l10.06-3.7a29.52,29.52,0,0,0,16.91-15.53,27.2,27.2,0,0,0,2.23-8.31,14.25,14.25,0,0,0-10.3-15c-8.42-2.48-16.86-1.84-21.7-1.1l8.93-7.23s6.64-4.83,38.84,8.05Z" opacity="0.2"/>
|
||||
|
||||
<path d="M512.56,561c8,4.49,17.84,3.92,26,.25,1.19-.51,2.33-1.14,3.54-1.66-1.08.73-2.18,1.45-3.3,2.13-8,4.53-18.78,5.1-26.25-.72Z" opacity="0.2"/>
|
||||
|
||||
<path d="M524.65,550.52c4.14,1.84,12.18,6.84,12.1,11.9-.72-2.81-3.3-4.59-5.35-6.46s-4.5-3.57-6.75-5.44Z" opacity="0.2"/>
|
||||
|
||||
<path d="M530.3,546.25c3.1.44,5.52,3.24,6.45,6.1-2.23-2.05-4-4.27-6.45-6.1Z" opacity="0.2"/>
|
||||
|
||||
<path d="M534.32,543.14a9.55,9.55,0,0,1,6.62,6.16c-2.17-2.19-4.35-4-6.62-6.16Z" opacity="0.2"/>
|
||||
|
||||
<path d="M538.89,539.61a11.81,11.81,0,0,1,5.36,6.61,29.31,29.31,0,0,1-5.36-6.61Z" opacity="0.2"/>
|
||||
|
||||
<rect x="421.88" y="364.26" width="328.48" height="7.51" fill="#b3b3b3"/>
|
||||
|
||||
<polygon points="750.36 364.26 750.36 371.77 786.86 342.79 786.88 336.37 750.36 364.26" fill="#999"/>
|
||||
|
||||
<path d="M507.76,344.93h98.07l-7.33-63.74a5.61,5.61,0,0,0-5.57-5h-90a3,3,0,0,0-2.93,3.31Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M605.83,344.93H507.76L500,279.54a3,3,0,0,1,2.95-3.31h90a5.61,5.61,0,0,1,5.56,5Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<polygon points="583.53 276.23 507.76 341.71 506.12 329.04 567.52 276.23 583.53 276.23" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M517.07,344.93l79.55-67.31a6,6,0,0,1,1.88,3.57l.38,3.34-71.09,60.4Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="507.76" y="344.93" width="94.46" height="6.44" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="602.22" y="344.93" width="29.49" height="6.44" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="602.22" y="344.93" width="29.49" height="6.44" opacity="0.2"/>
|
||||
|
||||
<polygon points="419.73 353.52 466.38 353.52 499.97 333.94 459.85 333.94 419.73 353.52" fill="#fff"/>
|
||||
|
||||
<rect x="419.73" y="353.52" width="46.65" height="4.65" fill="#e6e6e6"/>
|
||||
|
||||
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" fill="#ccc"/>
|
||||
|
||||
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" opacity="0.1"/>
|
||||
|
||||
<path d="M658.56,334.46s-13.47,1.87-20.95,12.08c0,0-10.05,9.15-.18,7.53,0,0,.47,4.68,8.39,1.53,0,0,1.37,3.31,10-1.53,0,0,8.64-4.84,16.16-7C672,347.08,675.17,334.71,658.56,334.46Z" fill="#f9b499"/>
|
||||
|
||||
<path d="M646,343.86a40.12,40.12,0,0,1-8.55,10.21A40.49,40.49,0,0,1,646,343.86Z" fill="#f7a48b"/>
|
||||
|
||||
<path d="M645.82,355.6a24.61,24.61,0,0,1,6.85-7.82,24.71,24.71,0,0,1-6.85,7.82Z" fill="#f7a48b"/>
|
||||
|
||||
<ellipse cx="638.72" cy="215.58" rx="6.44" ry="8.05" fill="#f9b499"/>
|
||||
|
||||
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M674.14,234.37c-5.73,6.95-13.48,12.06-21.25,16.49-1.15.58-2.28,1.2-3.44,1.76,8.36-5.92,17-11.41,24.69-18.25Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M683.8,238.66C679,244,671.85,246.84,664.89,248c6.47-2.57,13.26-5.24,18.91-9.35Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<circle cx="551.23" cy="311.12" r="8.05" fill="#fff"/>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 23 KiB |
BIN
public/assets/forrest-gump-quote.webp
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/assets/logo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
10
public/assets/logo.svg
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
public/astropaper-og.jpg
Normal file
After Width: | Height: | Size: 145 KiB |
13
public/favicon.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 873 B |
76
public/toggle-theme.js
Normal file
@ -0,0 +1,76 @@
|
||||
const primaryColorScheme = ""; // "light" | "dark"
|
||||
|
||||
// Get theme data from local storage
|
||||
const currentTheme = localStorage.getItem("theme");
|
||||
|
||||
function getPreferTheme() {
|
||||
// return theme value in local storage if it is set
|
||||
if (currentTheme) return currentTheme;
|
||||
|
||||
// return primary color scheme if it is set
|
||||
if (primaryColorScheme) return primaryColorScheme;
|
||||
|
||||
// return user device's prefer color scheme
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
let themeValue = getPreferTheme();
|
||||
|
||||
function setPreference() {
|
||||
localStorage.setItem("theme", themeValue);
|
||||
reflectPreference();
|
||||
}
|
||||
|
||||
function reflectPreference() {
|
||||
document.firstElementChild.setAttribute("data-theme", themeValue);
|
||||
|
||||
document.querySelector("#theme-btn")?.setAttribute("aria-label", themeValue);
|
||||
|
||||
// Get a reference to the body element
|
||||
const body = document.body;
|
||||
|
||||
// Check if the body element exists before using getComputedStyle
|
||||
if (body) {
|
||||
// Get the computed styles for the body element
|
||||
const computedStyles = window.getComputedStyle(body);
|
||||
|
||||
// Get the background color property
|
||||
const bgColor = computedStyles.backgroundColor;
|
||||
|
||||
// Set the background color in <meta theme-color ... />
|
||||
document
|
||||
.querySelector("meta[name='theme-color']")
|
||||
?.setAttribute("content", bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
// set early so no page flashes / CSS is made aware
|
||||
reflectPreference();
|
||||
|
||||
window.onload = () => {
|
||||
function setThemeFeature() {
|
||||
// set on load so screen readers can get the latest value on the button
|
||||
reflectPreference();
|
||||
|
||||
// now this script can find and listen for clicks on the control
|
||||
document.querySelector("#theme-btn")?.addEventListener("click", () => {
|
||||
themeValue = themeValue === "light" ? "dark" : "light";
|
||||
setPreference();
|
||||
});
|
||||
}
|
||||
|
||||
setThemeFeature();
|
||||
|
||||
// Runs on view transitions navigation
|
||||
document.addEventListener("astro:after-swap", setThemeFeature);
|
||||
};
|
||||
|
||||
// sync with system changes
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", ({ matches: isDark }) => {
|
||||
themeValue = isDark ? "dark" : "light";
|
||||
setPreference();
|
||||
});
|
1
remark-collapse.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'remark-collapse';
|
BIN
src/assets/images/AstroPaper-v3.png
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
src/assets/images/AstroPaper-v4.png
Normal file
After Width: | Height: | Size: 158 KiB |
211
src/assets/socialIcons.ts
Normal file
@ -0,0 +1,211 @@
|
||||
const socialIcons = {
|
||||
Github: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
|
||||
></path>
|
||||
</svg>`,
|
||||
Facebook: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M7 10v4h3v7h4v-7h3l1 -4h-4v-2a1 1 0 0 1 1 -1h3v-4h-3a5 5 0 0 0 -5 5v2h-3"
|
||||
></path>
|
||||
</svg>`,
|
||||
Instagram: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<rect x="4" y="4" width="16" height="16" rx="4"></rect>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<line x1="16.5" y1="7.5" x2="16.5" y2="7.501"></line>
|
||||
</svg>`,
|
||||
LinkedIn: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||
<line x1="8" y1="11" x2="8" y2="16"></line>
|
||||
<line x1="8" y1="8" x2="8" y2="8.01"></line>
|
||||
<line x1="12" y1="16" x2="12" y2="11"></line>
|
||||
<path d="M16 16v-3a2 2 0 0 0 -4 0"></path>
|
||||
</svg>`,
|
||||
Mail: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
|
||||
<polyline points="3 7 12 13 21 7"></polyline>
|
||||
</svg>`,
|
||||
X: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 4l11.733 16h4.267l-11.733 -16z" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />
|
||||
</svg>`,
|
||||
Twitch: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"></path>
|
||||
</svg>`,
|
||||
YouTube: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path>
|
||||
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon>
|
||||
</svg>`,
|
||||
WhatsApp: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9"></path>
|
||||
<path d="M9 10a0.5 .5 0 0 0 1 0v-1a0.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a0.5 .5 0 0 0 0 -1h-1a0.5 .5 0 0 0 0 1"></path>
|
||||
</svg>`,
|
||||
Snapchat: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M16.882 7.842a4.882 4.882 0 0 0 -9.764 0c0 4.273 -.213 6.409 -4.118 8.118c2 .882 2 .882 3 3c3 0 4 2 6 2s3 -2 6 -2c1 -2.118 1 -2.118 3 -3c-3.906 -1.709 -4.118 -3.845 -4.118 -8.118zm-13.882 8.119c4 -2.118 4 -4.118 1 -7.118m17 7.118c-4 -2.118 -4 -4.118 -1 -7.118"></path>
|
||||
</svg>`,
|
||||
Pinterest: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<line x1="8" y1="20" x2="12" y2="11"></line>
|
||||
<path d="M10.7 14c.437 1.263 1.43 2 2.55 2c2.071 0 3.75 -1.554 3.75 -4a5 5 0 1 0 -9.7 1.7"></path>
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
</svg>`,
|
||||
TikTok: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 12a4 4 0 1 0 4 4v-12a5 5 0 0 0 5 5"></path>
|
||||
</svg>`,
|
||||
CodePen: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 15l9 6l9 -6l-9 -6l-9 6"></path>
|
||||
<path d="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
|
||||
<line x1="3" y1="9" x2="3" y2="15"></line>
|
||||
<line x1="21" y1="9" x2="21" y2="15"></line>
|
||||
<line x1="12" y1="3" x2="12" y2="9"></line>
|
||||
<line x1="12" y1="15" x2="12" y2="21"></line>
|
||||
</svg>`,
|
||||
Discord: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<circle cx="9" cy="12" r="1"></circle>
|
||||
<circle cx="15" cy="12" r="1"></circle>
|
||||
<path d="M7.5 7.5c3.5 -1 5.5 -1 9 0"></path>
|
||||
<path d="M7 16.5c3.5 1 6.5 1 10 0"></path>
|
||||
<path d="M15.5 17c0 1 1.5 3 2 3c1.5 0 2.833 -1.667 3.5 -3c.667 -1.667 .5 -5.833 -1.5 -11.5c-1.457 -1.015 -3 -1.34 -4.5 -1.5l-1 2.5"></path>
|
||||
<path d="M8.5 17c0 1 -1.356 3 -1.832 3c-1.429 0 -2.698 -1.667 -3.333 -3c-.635 -1.667 -.476 -5.833 1.428 -11.5c1.388 -1.015 2.782 -1.34 4.237 -1.5l1 2.5"></path>
|
||||
</svg>`,
|
||||
GitLab: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M21 14l-9 7l-9 -7l3 -11l3 7h6l3 -7z"></path>
|
||||
</svg>`,
|
||||
Reddit: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 8c2.648 0 5.028 .826 6.675 2.14a2.5 2.5 0 0 1 2.326 4.36c0 3.59 -4.03 6.5 -9 6.5c-4.875 0 -8.845 -2.8 -9 -6.294l-1 -.206a2.5 2.5 0 0 1 2.326 -4.36c1.646 -1.313 4.026 -2.14 6.674 -2.14z"></path>
|
||||
<path d="M12 8l1 -5l6 1"></path>
|
||||
<circle cx="19" cy="4" r="1"></circle>
|
||||
<circle cx="9" cy="13" r=".5" fill="currentColor"></circle>
|
||||
<circle cx="15" cy="13" r=".5" fill="currentColor"></circle>
|
||||
<path d="M10 17c.667 .333 1.333 .5 2 .5s1.333 -.167 2 -.5"></path>
|
||||
</svg>`,
|
||||
Skype: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 3a9 9 0 0 1 8.603 11.65a4.5 4.5 0 0 1 -5.953 5.953a9 9 0 0 1 -11.253 -11.253a4.5 4.5 0 0 1 5.953 -5.954a8.987 8.987 0 0 1 2.65 -.396z"></path>
|
||||
<path d="M8 14.5c.5 2 2.358 2.5 4 2.5c2.905 0 4 -1.187 4 -2.5c0 -1.503 -1.927 -2.5 -4 -2.5s-4 -.997 -4 -2.5c0 -1.313 1.095 -2.5 4 -2.5c1.642 0 3.5 .5 4 2.5"></path>
|
||||
</svg>`,
|
||||
Steam: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M16.5 5a4.5 4.5 0 1 1 -.653 8.953l-4.347 3.009l0 .038a3 3 0 0 1 -2.824 2.995l-.176 .005a3 3 0 0 1 -2.94 -2.402l-2.56 -1.098v-3.5l3.51 1.755a2.989 2.989 0 0 1 2.834 -.635l2.727 -3.818a4.5 4.5 0 0 1 4.429 -5.302z"></path>
|
||||
<circle fill="currentColor" cx="16.5" cy="9.5" r="1"></circle>
|
||||
</svg>`,
|
||||
Telegram: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4"></path>
|
||||
</svg>`,
|
||||
Mastodon: `<svg class="icon-tabler" viewBox="-10 -5 1034 1034" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<path fill="currentColor"
|
||||
d="M499 112q-93 1 -166 11q-81 11 -128 33l-14 8q-16 10 -32 25q-22 21 -38 47q-21 33 -32 73q-14 47 -14 103v37q0 77 1 119q3 113 18 188q19 95 62 154q50 67 134 89q109 29 210 24q46 -3 88 -12q30 -7 55 -17l19 -8l-4 -75l-22 6q-28 6 -57 10q-41 6 -78 4q-53 -1 -80 -7
|
||||
q-43 -8 -67 -30q-29 -25 -35 -72q-2 -14 -2 -29l25 6q31 6 65 10q48 7 93 9q42 2 92 -2q32 -2 88 -9t107 -30q49 -23 81.5 -54.5t38.5 -63.5q9 -45 13 -109q4 -46 5 -97v-41q0 -56 -14 -103q-11 -40 -32 -73q-16 -26 -38 -47q-15 -15 -32 -25q-12 -8 -14 -8
|
||||
q-46 -22 -127 -33q-74 -10 -166 -11h-3zM367 267q73 0 109 56l24 39l24 -39q36 -56 109 -56q63 0 101 43t38 117v239h-95v-232q0 -74 -61 -74q-69 0 -69 88v127h-94v-127q0 -88 -69 -88q-61 0 -61 74v232h-95v-239q0 -74 38 -117t101 -43z" />
|
||||
</svg>`,
|
||||
};
|
||||
|
||||
export default socialIcons;
|
72
src/components/Breadcrumbs.astro
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
// Remove current url path and remove trailing slash if exists
|
||||
const currentUrlPath = Astro.url.pathname.replace(/\/+$/, "");
|
||||
|
||||
// Get url array from path
|
||||
// eg: /tags/tailwindcss => ['tags', 'tailwindcss']
|
||||
const breadcrumbList = currentUrlPath.split("/").slice(1);
|
||||
|
||||
// if breadcrumb is Home > Posts > 1 <etc>
|
||||
// replace Posts with Posts (page number)
|
||||
breadcrumbList[0] === "posts" &&
|
||||
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
|
||||
|
||||
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
|
||||
// replace [tag] > [page] with [tag] (page number)
|
||||
breadcrumbList[0] === "tags" &&
|
||||
!isNaN(Number(breadcrumbList[2])) &&
|
||||
breadcrumbList.splice(
|
||||
1,
|
||||
3,
|
||||
`${breadcrumbList[1]} ${
|
||||
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
|
||||
}`
|
||||
);
|
||||
---
|
||||
|
||||
<nav class="breadcrumb" aria-label="breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
<span aria-hidden="true">»</span>
|
||||
</li>
|
||||
{
|
||||
breadcrumbList.map((breadcrumb, index) =>
|
||||
index + 1 === breadcrumbList.length ? (
|
||||
<li>
|
||||
<span
|
||||
class={`${index > 0 ? "lowercase" : "capitalize"}`}
|
||||
aria-current="page"
|
||||
>
|
||||
{/* make the last part lowercase in Home > Tags > some-tag */}
|
||||
{decodeURIComponent(breadcrumb)}
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<a href={`/${breadcrumb}/`}>{breadcrumb}</a>
|
||||
<span aria-hidden="true">»</span>
|
||||
</li>
|
||||
)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.breadcrumb {
|
||||
@apply mx-auto mb-1 mt-8 w-full max-w-3xl px-4;
|
||||
}
|
||||
.breadcrumb ul li {
|
||||
@apply inline;
|
||||
}
|
||||
.breadcrumb ul li a {
|
||||
@apply capitalize opacity-70;
|
||||
}
|
||||
.breadcrumb ul li span {
|
||||
@apply opacity-70;
|
||||
}
|
||||
.breadcrumb ul li:not(:last-child) a {
|
||||
@apply hover:opacity-100;
|
||||
}
|
||||
</style>
|
35
src/components/Card.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { slugifyStr } from "@utils/slugify";
|
||||
import Datetime from "./Datetime";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
href?: string;
|
||||
frontmatter: CollectionEntry<"blog">["data"];
|
||||
secHeading?: boolean;
|
||||
}
|
||||
|
||||
export default function Card({ href, frontmatter, secHeading = true }: Props) {
|
||||
const { title, pubDatetime, modDatetime, description } = frontmatter;
|
||||
|
||||
const headerProps = {
|
||||
style: { viewTransitionName: slugifyStr(title) },
|
||||
className: "text-lg font-medium decoration-dashed hover:underline",
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="my-6">
|
||||
<a
|
||||
href={href}
|
||||
className="inline-block text-lg font-medium text-skin-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
|
||||
>
|
||||
{secHeading ? (
|
||||
<h2 {...headerProps}>{title}</h2>
|
||||
) : (
|
||||
<h3 {...headerProps}>{title}</h3>
|
||||
)}
|
||||
</a>
|
||||
<Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
|
||||
<p>{description}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
120
src/components/Datetime.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { LOCALE, SITE } from "@config";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface DatetimesProps {
|
||||
pubDatetime: string | Date;
|
||||
modDatetime: string | Date | undefined | null;
|
||||
}
|
||||
|
||||
interface EditPostProps {
|
||||
editPost?: CollectionEntry<"blog">["data"]["editPost"];
|
||||
postId?: CollectionEntry<"blog">["id"];
|
||||
}
|
||||
|
||||
interface Props extends DatetimesProps, EditPostProps {
|
||||
size?: "sm" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Datetime({
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
size = "sm",
|
||||
className = "",
|
||||
editPost,
|
||||
postId,
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center space-x-2 opacity-80 ${className}`.trim()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${
|
||||
size === "sm" ? "scale-90" : "scale-100"
|
||||
} inline-block h-6 w-6 min-w-[1.375rem] fill-skin-base`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path>
|
||||
<path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path>
|
||||
</svg>
|
||||
{modDatetime && modDatetime > pubDatetime ? (
|
||||
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
|
||||
Updated:
|
||||
</span>
|
||||
) : (
|
||||
<span className="sr-only">Published:</span>
|
||||
)}
|
||||
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
|
||||
<FormattedDatetime
|
||||
pubDatetime={pubDatetime}
|
||||
modDatetime={modDatetime}
|
||||
/>
|
||||
{size === "lg" && <EditPost editPost={editPost} postId={postId} />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FormattedDatetime = ({ pubDatetime, modDatetime }: DatetimesProps) => {
|
||||
const myDatetime = new Date(
|
||||
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
|
||||
);
|
||||
|
||||
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<time dateTime={myDatetime.toISOString()}>{date}</time>
|
||||
<span aria-hidden="true"> | </span>
|
||||
<span className="sr-only"> at </span>
|
||||
<span className="text-nowrap">{time}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditPost = ({ editPost, postId }: EditPostProps) => {
|
||||
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
|
||||
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
|
||||
const appendFilePath =
|
||||
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
|
||||
if (appendFilePath && postId) {
|
||||
editPostUrl += `/${postId}`;
|
||||
}
|
||||
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
|
||||
|
||||
return (
|
||||
showEditPost && (
|
||||
<>
|
||||
<span aria-hidden="true"> | </span>
|
||||
<a
|
||||
className="space-x-1.5 hover:opacity-75"
|
||||
href={editPostUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon icon-tabler icons-tabler-outline icon-tabler-edit inline-block !scale-90 fill-skin-base"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
|
||||
<path d="M16 5l3 3" />
|
||||
</svg>
|
||||
<span className="text-base italic">{editPostText}</span>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
45
src/components/Footer.astro
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
import Hr from "./Hr.astro";
|
||||
import Socials from "./Socials.astro";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export interface Props {
|
||||
noMarginTop?: boolean;
|
||||
}
|
||||
|
||||
const { noMarginTop = false } = Astro.props;
|
||||
---
|
||||
|
||||
<footer class={`${noMarginTop ? "" : "mt-auto"}`}>
|
||||
<Hr noPadding />
|
||||
<div class="footer-wrapper">
|
||||
<Socials centered />
|
||||
<div class="copyright-wrapper">
|
||||
<span>Copyright © {currentYear}</span>
|
||||
<span class="separator"> | </span>
|
||||
<span>All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
@apply w-full;
|
||||
}
|
||||
.footer-wrapper {
|
||||
@apply flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4;
|
||||
}
|
||||
.link-button {
|
||||
@apply my-1 p-2 hover:rotate-6;
|
||||
}
|
||||
.link-button svg {
|
||||
@apply scale-125;
|
||||
}
|
||||
.copyright-wrapper {
|
||||
@apply my-2 flex flex-col items-center whitespace-nowrap sm:flex-row;
|
||||
}
|
||||
.separator {
|
||||
@apply hidden sm:inline;
|
||||
}
|
||||
</style>
|
266
src/components/Header.astro
Normal file
@ -0,0 +1,266 @@
|
||||
---
|
||||
import { LOGO_IMAGE, SITE } from "@config";
|
||||
import Hr from "./Hr.astro";
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
|
||||
export interface Props {
|
||||
activeNav?: "posts" | "archives" | "tags" | "about" | "search";
|
||||
}
|
||||
|
||||
const { activeNav } = Astro.props;
|
||||
---
|
||||
|
||||
<header>
|
||||
<a id="skip-to-content" href="#main-content">Skip to content</a>
|
||||
<div class="nav-container">
|
||||
<div class="top-nav-wrap">
|
||||
<a href="/" class="logo whitespace-nowrap">
|
||||
{
|
||||
LOGO_IMAGE.enable ? (
|
||||
<img
|
||||
src={`/assets/${LOGO_IMAGE.svg ? "logo.svg" : "logo.png"}`}
|
||||
alt={SITE.title}
|
||||
width={LOGO_IMAGE.width}
|
||||
height={LOGO_IMAGE.height}
|
||||
/>
|
||||
) : (
|
||||
SITE.title
|
||||
)
|
||||
}
|
||||
</a>
|
||||
|
||||
<nav id="nav-menu">
|
||||
<button
|
||||
class="hamburger-menu focus-outline"
|
||||
aria-label="Open Menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="menu-items"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="menu-icon"
|
||||
>
|
||||
<line x1="7" y1="12" x2="21" y2="12" class="line"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6" class="line"></line>
|
||||
<line x1="12" y1="18" x2="21" y2="18" class="line"></line>
|
||||
<line x1="18" y1="6" x2="6" y2="18" class="close"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18" class="close"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<ul id="menu-items" class="display-none sm:flex">
|
||||
<li>
|
||||
<a href="/posts/" class={activeNav === "posts" ? "active" : ""}>
|
||||
Posts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/tags/" class={activeNav === "tags" ? "active" : ""}>
|
||||
Tags
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{
|
||||
SITE.showArchives && (
|
||||
<li>
|
||||
<LinkButton
|
||||
href="/archives/"
|
||||
className={`focus-outline flex justify-center p-3 sm:p-1`}
|
||||
ariaLabel="archives"
|
||||
title="Archives"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class:list={[
|
||||
"icon icon-tabler icons-tabler-outline !hidden sm:!inline-block",
|
||||
activeNav === "archives" && "!stroke-skin-accent",
|
||||
]}
|
||||
>
|
||||
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" />
|
||||
<path d="M10 12l4 0" />
|
||||
|
||||
</svg>
|
||||
<span
|
||||
class:list={[
|
||||
"sm:sr-only",
|
||||
activeNav === "archives" && "active",
|
||||
]}
|
||||
>
|
||||
Archives
|
||||
</span>
|
||||
</LinkButton>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
<li>
|
||||
<LinkButton
|
||||
href="/search/"
|
||||
className={`focus-outline p-3 sm:p-1 ${
|
||||
activeNav === "search" ? "active" : ""
|
||||
} flex`}
|
||||
ariaLabel="search"
|
||||
title="Search"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="scale-125 sm:scale-100"
|
||||
><path
|
||||
d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="sr-only">Search</span>
|
||||
</LinkButton>
|
||||
</li>
|
||||
<li>
|
||||
<LinkButton
|
||||
href="/rss.xml"
|
||||
className={`focus-outline p-3 sm:p-1 ${
|
||||
activeNav === "search" ? "active" : ""
|
||||
} flex`}
|
||||
ariaLabel="rss feed"
|
||||
title="RSS Feed"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="rss-icon"
|
||||
><path
|
||||
d="M19 20.001C19 11.729 12.271 5 4 5v2c7.168 0 13 5.832 13 13.001h2z"
|
||||
></path><path
|
||||
d="M12 20.001h2C14 14.486 9.514 10 4 10v2c4.411 0 8 3.589 8 8.001z"
|
||||
></path><circle cx="6" cy="18" r="2"></circle>
|
||||
</svg>
|
||||
<span class="sr-only">RSS Feed</span>
|
||||
</LinkButton>
|
||||
</li>
|
||||
{
|
||||
SITE.lightAndDarkMode && (
|
||||
<li>
|
||||
<button
|
||||
id="theme-btn"
|
||||
class="focus-outline"
|
||||
title="Toggles light & dark"
|
||||
aria-label="auto"
|
||||
aria-live="polite"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="moon-svg">
|
||||
<path 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" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="sun-svg">
|
||||
<path 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" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<Hr />
|
||||
</header>
|
||||
|
||||
<style>
|
||||
#skip-to-content {
|
||||
@apply absolute -top-full left-16 z-50 bg-skin-accent px-3 py-2 text-skin-inverted transition-all focus:top-4;
|
||||
}
|
||||
.nav-container {
|
||||
@apply mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row;
|
||||
}
|
||||
.top-nav-wrap {
|
||||
@apply relative flex w-full items-start justify-between p-4 sm:items-center sm:py-8;
|
||||
}
|
||||
.logo {
|
||||
@apply absolute py-1 text-xl font-semibold sm:static sm:text-2xl;
|
||||
}
|
||||
.hamburger-menu {
|
||||
@apply self-end p-2 sm:hidden;
|
||||
}
|
||||
.hamburger-menu svg {
|
||||
@apply h-6 w-6 scale-125 fill-skin-base;
|
||||
}
|
||||
|
||||
nav {
|
||||
@apply flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0;
|
||||
}
|
||||
nav ul {
|
||||
@apply mt-4 grid w-44 grid-cols-2 grid-rows-4 gap-x-2 gap-y-2 sm:ml-0 sm:mt-0 sm:w-auto sm:gap-x-5 sm:gap-y-0;
|
||||
}
|
||||
nav ul li {
|
||||
@apply col-span-2 flex items-center justify-center;
|
||||
}
|
||||
nav ul li a {
|
||||
@apply w-full px-4 py-3 text-center font-medium hover:text-skin-accent sm:my-0 sm:px-2 sm:py-1;
|
||||
}
|
||||
nav ul li:nth-last-child(2) a {
|
||||
@apply w-auto;
|
||||
}
|
||||
nav ul li:nth-last-child(1),
|
||||
nav ul li:nth-last-child(2) {
|
||||
@apply col-span-1;
|
||||
}
|
||||
nav .active {
|
||||
@apply underline decoration-wavy decoration-2 underline-offset-4;
|
||||
}
|
||||
nav a.active svg {
|
||||
@apply fill-skin-accent;
|
||||
}
|
||||
|
||||
nav button {
|
||||
@apply p-1;
|
||||
}
|
||||
nav button svg {
|
||||
@apply h-6 w-6 fill-skin-base hover:fill-skin-accent;
|
||||
}
|
||||
#theme-btn {
|
||||
@apply p-3 sm:p-1;
|
||||
}
|
||||
#theme-btn svg {
|
||||
@apply scale-125 hover:rotate-12 sm:scale-100;
|
||||
}
|
||||
|
||||
.menu-icon line {
|
||||
@apply transition-opacity duration-75 ease-in-out;
|
||||
}
|
||||
.menu-icon .close {
|
||||
opacity: 0;
|
||||
}
|
||||
.menu-icon.is-active .line {
|
||||
@apply opacity-0;
|
||||
}
|
||||
.menu-icon.is-active .close {
|
||||
@apply opacity-100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleNav() {
|
||||
// Toggle menu
|
||||
const menuBtn = document.querySelector(".hamburger-menu");
|
||||
const menuIcon = document.querySelector(".menu-icon");
|
||||
const menuItems = document.querySelector("#menu-items");
|
||||
|
||||
menuBtn?.addEventListener("click", () => {
|
||||
const menuExpanded = menuBtn.getAttribute("aria-expanded") === "true";
|
||||
menuIcon?.classList.toggle("is-active");
|
||||
menuBtn.setAttribute("aria-expanded", menuExpanded ? "false" : "true");
|
||||
menuBtn.setAttribute(
|
||||
"aria-label",
|
||||
menuExpanded ? "Open Menu" : "Close Menu"
|
||||
);
|
||||
menuItems?.classList.toggle("display-none");
|
||||
});
|
||||
}
|
||||
|
||||
toggleNav();
|
||||
|
||||
// Runs on view transitions navigation
|
||||
document.addEventListener("astro:after-swap", toggleNav);
|
||||
</script>
|
12
src/components/Hr.astro
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
export interface Props {
|
||||
noPadding?: boolean;
|
||||
ariaHidden?: boolean;
|
||||
}
|
||||
|
||||
const { noPadding = false, ariaHidden = true } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`max-w-3xl mx-auto ${noPadding ? "px-0" : "px-4"}`}>
|
||||
<hr class="border-skin-line" aria-hidden={ariaHidden} />
|
||||
</div>
|
38
src/components/LinkButton.astro
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
export interface Props {
|
||||
href: string;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
href,
|
||||
className = "",
|
||||
ariaLabel,
|
||||
title,
|
||||
disabled = false,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
disabled ? (
|
||||
<span
|
||||
class:list={["group inline-block", className]}
|
||||
title={title}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
{href}
|
||||
class:list={["group inline-block hover:text-skin-accent", className]}
|
||||
aria-label={ariaLabel}
|
||||
title={title}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
)
|
||||
}
|
59
src/components/Pagination.astro
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
import type { Page } from "astro";
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
page: Page<CollectionEntry<"blog">>;
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
page.lastPage > 1 && (
|
||||
<nav class="pagination-wrapper" aria-label="Pagination">
|
||||
<LinkButton
|
||||
disabled={!page.url.prev}
|
||||
href={page.url.prev as string}
|
||||
className={`mr-4 select-none ${page.url.prev ? "" : "disabled"}`}
|
||||
ariaLabel="Previous"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class:list={[{ "disabled-svg": !page.url.prev }]}
|
||||
>
|
||||
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
|
||||
</svg>
|
||||
Prev
|
||||
</LinkButton>
|
||||
{page.currentPage} / {page.lastPage}
|
||||
<LinkButton
|
||||
disabled={!page.url.next}
|
||||
href={page.url.next as string}
|
||||
className={`mx-4 select-none ${page.url.next ? "" : "disabled"}`}
|
||||
ariaLabel="Next"
|
||||
>
|
||||
Next
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class:list={[{ "disabled-svg": !page.url.next }]}
|
||||
>
|
||||
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
.pagination-wrapper {
|
||||
@apply mb-8 mt-auto flex justify-center;
|
||||
}
|
||||
.disabled {
|
||||
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
|
||||
}
|
||||
.disabled-svg {
|
||||
@apply group-hover:!fill-skin-base;
|
||||
}
|
||||
</style>
|
127
src/components/Search.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
|
||||
import Card from "@components/Card";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export type SearchItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
data: CollectionEntry<"blog">["data"];
|
||||
slug: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
searchList: SearchItem[];
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
item: SearchItem;
|
||||
refIndex: number;
|
||||
}
|
||||
|
||||
export default function SearchBar({ searchList }: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputVal, setInputVal] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
setInputVal(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const fuse = useMemo(
|
||||
() =>
|
||||
new Fuse(searchList, {
|
||||
keys: ["title", "description"],
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 2,
|
||||
threshold: 0.5,
|
||||
}),
|
||||
[searchList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if URL has search query,
|
||||
// insert that search query in input field
|
||||
const searchUrl = new URLSearchParams(window.location.search);
|
||||
const searchStr = searchUrl.get("q");
|
||||
if (searchStr) setInputVal(searchStr);
|
||||
|
||||
// put focus cursor at the end of the string
|
||||
setTimeout(function () {
|
||||
inputRef.current!.selectionStart = inputRef.current!.selectionEnd =
|
||||
searchStr?.length || 0;
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Add search result only if
|
||||
// input value is more than one character
|
||||
const inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
|
||||
setSearchResults(inputResult);
|
||||
|
||||
// Update search string in URL
|
||||
if (inputVal.length > 0) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
searchParams.set("q", inputVal);
|
||||
const newRelativePathQuery =
|
||||
window.location.pathname + "?" + searchParams.toString();
|
||||
history.replaceState(history.state, "", newRelativePathQuery);
|
||||
} else {
|
||||
history.replaceState(history.state, "", window.location.pathname);
|
||||
}
|
||||
}, [inputVal]);
|
||||
|
||||
useEffect(() => {
|
||||
// focus on text input when search bar is displayed
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputVal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="relative block">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2 opacity-75">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
|
||||
</svg>
|
||||
<span className="sr-only">Search</span>
|
||||
</span>
|
||||
<input
|
||||
className="block w-full rounded border border-skin-fill/40 bg-skin-fill py-3 pl-10 pr-3 placeholder:italic focus:border-skin-accent focus:outline-none"
|
||||
placeholder="Search for anything..."
|
||||
type="text"
|
||||
name="search"
|
||||
value={inputVal}
|
||||
onChange={handleChange}
|
||||
autoComplete="off"
|
||||
// autoFocus
|
||||
ref={inputRef}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{inputVal.length > 1 && (
|
||||
<div className="mt-8">
|
||||
Found {searchResults?.length}
|
||||
{searchResults?.length && searchResults?.length === 1
|
||||
? " result"
|
||||
: " results"}{" "}
|
||||
for '{inputVal}'
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul>
|
||||
{searchResults &&
|
||||
searchResults.map(({ item, refIndex }) => (
|
||||
<Card
|
||||
href={`/posts/${item.slug}/`}
|
||||
frontmatter={item.data}
|
||||
key={`${refIndex}-${item.slug}`}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
66
src/components/ShareLinks.astro
Normal file
@ -0,0 +1,66 @@
|
||||
---
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
import socialIcons from "@assets/socialIcons";
|
||||
|
||||
const URL = Astro.url;
|
||||
|
||||
const shareLinks = [
|
||||
{
|
||||
name: "WhatsApp",
|
||||
href: "https://wa.me/?text=",
|
||||
linkTitle: `Share this post via WhatsApp`,
|
||||
},
|
||||
{
|
||||
name: "Facebook",
|
||||
href: "https://www.facebook.com/sharer.php?u=",
|
||||
linkTitle: `Share this post on Facebook`,
|
||||
},
|
||||
{
|
||||
name: "X",
|
||||
href: "https://x.com/intent/post?url=",
|
||||
linkTitle: `Share this post on X`,
|
||||
},
|
||||
{
|
||||
name: "Telegram",
|
||||
href: "https://t.me/share/url?url=",
|
||||
linkTitle: `Share this post via Telegram`,
|
||||
},
|
||||
{
|
||||
name: "Pinterest",
|
||||
href: "https://pinterest.com/pin/create/button/?url=",
|
||||
linkTitle: `Share this post on Pinterest`,
|
||||
},
|
||||
{
|
||||
name: "Mail",
|
||||
href: "mailto:?subject=See%20this%20post&body=",
|
||||
linkTitle: `Share this post via email`,
|
||||
},
|
||||
] as const;
|
||||
---
|
||||
|
||||
<div class={`social-icons`}>
|
||||
<span class="italic">Share this post on:</span>
|
||||
<div class="text-center">
|
||||
{
|
||||
shareLinks.map(social => (
|
||||
<LinkButton
|
||||
href={`${social.href + URL}`}
|
||||
className="link-button"
|
||||
title={social.linkTitle}
|
||||
>
|
||||
<Fragment set:html={socialIcons[social.name]} />
|
||||
<span class="sr-only">{social.linkTitle}</span>
|
||||
</LinkButton>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.social-icons {
|
||||
@apply flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start;
|
||||
}
|
||||
.link-button {
|
||||
@apply scale-90 p-2 hover:rotate-6 sm:p-1;
|
||||
}
|
||||
</style>
|
35
src/components/Socials.astro
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
import { SOCIALS } from "@config";
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
import socialIcons from "@assets/socialIcons";
|
||||
|
||||
export interface Props {
|
||||
centered?: boolean;
|
||||
}
|
||||
|
||||
const { centered = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`social-icons ${centered ? "flex" : ""}`}>
|
||||
{
|
||||
SOCIALS.filter(social => social.active).map(social => (
|
||||
<LinkButton
|
||||
href={social.href}
|
||||
className="link-button"
|
||||
title={social.linkTitle}
|
||||
>
|
||||
<Fragment set:html={socialIcons[social.name]} />
|
||||
<span class="sr-only">{social.linkTitle}</span>
|
||||
</LinkButton>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.social-icons {
|
||||
@apply flex-wrap justify-center gap-1;
|
||||
}
|
||||
.link-button {
|
||||
@apply p-2 hover:rotate-6 sm:p-1;
|
||||
}
|
||||
</style>
|
38
src/components/Tag.astro
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
export interface Props {
|
||||
tag: string;
|
||||
size?: "sm" | "lg";
|
||||
}
|
||||
|
||||
const { tag, size = "sm" } = Astro.props;
|
||||
---
|
||||
|
||||
<li
|
||||
class={`inline-block ${
|
||||
size === "sm" ? "my-1 underline-offset-4" : "my-3 mx-1 underline-offset-8"
|
||||
}`}
|
||||
>
|
||||
<a
|
||||
href={`/tags/${tag}/`}
|
||||
transition:name={tag}
|
||||
class={`${size === "sm" ? "text-sm" : "text-lg"} pr-2 group`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={`${size === "sm" ? " scale-75" : "scale-110"}`}
|
||||
><path
|
||||
d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{tag}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
a {
|
||||
@apply relative underline decoration-dashed hover:-top-0.5 hover:text-skin-accent focus-visible:p-1;
|
||||
}
|
||||
a svg {
|
||||
@apply -mr-5 h-6 w-6 scale-95 text-skin-base opacity-80 group-hover:fill-skin-accent;
|
||||
}
|
||||
</style>
|
155
src/config.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import type { Site, SocialObjects } from "./types";
|
||||
|
||||
export const SITE: Site = {
|
||||
website: "https://blog.mohamad.dev/", // replace this with your deployed domain
|
||||
author: "Mohamad",
|
||||
profile: "https://mohamad.dev/",
|
||||
desc: "a blog.",
|
||||
title: "Mohamad",
|
||||
ogImage: "astropaper-og.jpg",
|
||||
lightAndDarkMode: true,
|
||||
postPerIndex: 4,
|
||||
postPerPage: 3,
|
||||
scheduledPostMargin: 15 * 60 * 1000, // 15 minutes
|
||||
showArchives: false,
|
||||
editPost: {
|
||||
url: "https://github.com/satnaing/astro-paper/edit/main/src/content/blog",
|
||||
text: "Suggest Changes",
|
||||
appendFilePath: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LOCALE = {
|
||||
lang: "en", // html lang code. Set this empty and default will be "en"
|
||||
langTag: ["en-EN"], // BCP 47 Language Tags. Set this empty [] to use the environment default
|
||||
} as const;
|
||||
|
||||
export const LOGO_IMAGE = {
|
||||
enable: true,
|
||||
svg: false,
|
||||
width: 75,
|
||||
height: 6,
|
||||
};
|
||||
|
||||
export const SOCIALS: SocialObjects = [
|
||||
{
|
||||
name: "Github",
|
||||
href: "https://git.mohamad.dev/mo",
|
||||
linkTitle: ` ${SITE.title} on Github`,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: "Facebook",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Facebook`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Instagram",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Instagram`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "LinkedIn",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on LinkedIn`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Mail",
|
||||
href: "mailto:mohamad@elsena.me",
|
||||
linkTitle: `Send an email to ${SITE.title}`,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: "X",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on X`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Twitch",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Twitch`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "YouTube",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on YouTube`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "WhatsApp",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on WhatsApp`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Snapchat",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Snapchat`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Pinterest",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Pinterest`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "TikTok",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on TikTok`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "CodePen",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on CodePen`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Discord`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "GitLab",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on GitLab`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Reddit",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Reddit`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Skype",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Skype`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Steam",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Steam`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Telegram",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Telegram`,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: "Mastodon",
|
||||
href: "https://github.com/satnaing/astro-paper",
|
||||
linkTitle: `${SITE.title} on Mastodon`,
|
||||
active: false,
|
||||
},
|
||||
];
|
17
src/content/blog/hi.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
author: Mohamad Elsena
|
||||
pubDatetime: 2025-02-04T10:15:00Z
|
||||
modDatetime: 2025-02-04T10:15:47.400Z
|
||||
title: Hi :)
|
||||
slug: hi and welcome
|
||||
featured: false
|
||||
draft: false
|
||||
tags:
|
||||
- welcome
|
||||
description:
|
||||
This is a welcome/test post. Happy to have you here! :)
|
||||
---
|
||||
|
||||
# {{ title }}
|
||||
|
||||
Welcome to my first post about Astro! 🚀
|
36
src/content/config.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { SITE } from "@config";
|
||||
import { glob } from "astro/loaders";
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const blog = defineCollection({
|
||||
type: "content_layer",
|
||||
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
author: z.string().default(SITE.author),
|
||||
pubDatetime: z.date(),
|
||||
modDatetime: z.date().optional().nullable(),
|
||||
title: z.string(),
|
||||
featured: z.boolean().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
tags: z.array(z.string()).default(["others"]),
|
||||
ogImage: image()
|
||||
.refine(img => img.width >= 1200 && img.height >= 630, {
|
||||
message: "OpenGraph image must be at least 1200 X 630 pixels!",
|
||||
})
|
||||
.or(z.string())
|
||||
.optional(),
|
||||
description: z.string(),
|
||||
canonicalURL: z.string().optional(),
|
||||
editPost: z
|
||||
.object({
|
||||
disabled: z.boolean().optional(),
|
||||
url: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
appendFilePath: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
2
src/env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
28
src/layouts/AboutLayout.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import { SITE } from "@config";
|
||||
import Breadcrumbs from "@components/Breadcrumbs.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Layout from "./Layout.astro";
|
||||
|
||||
export interface Props {
|
||||
frontmatter: {
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`${frontmatter.title} | ${SITE.title}`}>
|
||||
<Header activeNav="about" />
|
||||
<Breadcrumbs />
|
||||
<main id="main-content">
|
||||
<section id="about" class="prose mb-28 max-w-3xl prose-img:border-0">
|
||||
<h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
142
src/layouts/Layout.astro
Normal file
@ -0,0 +1,142 @@
|
||||
---
|
||||
import { LOCALE, SITE } from "@config";
|
||||
import "@styles/base.css";
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
|
||||
const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
author?: string;
|
||||
profile?: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
pubDatetime?: Date;
|
||||
modDatetime?: Date | null;
|
||||
scrollSmooth?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
title = SITE.title,
|
||||
author = SITE.author,
|
||||
profile = SITE.profile,
|
||||
description = SITE.desc,
|
||||
ogImage = SITE.ogImage,
|
||||
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
scrollSmooth = false,
|
||||
} = Astro.props;
|
||||
|
||||
const socialImageURL = new URL(
|
||||
ogImage ?? SITE.ogImage ?? "og.png",
|
||||
Astro.url.origin
|
||||
).href;
|
||||
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: `${title}`,
|
||||
image: `${socialImageURL}`,
|
||||
datePublished: `${pubDatetime?.toISOString()}`,
|
||||
...(modDatetime && { dateModified: modDatetime.toISOString() }),
|
||||
author: [
|
||||
{
|
||||
"@type": "Person",
|
||||
name: `${author}`,
|
||||
url: `${profile}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html
|
||||
lang=`${LOCALE.lang ?? "en"}`
|
||||
class={`${scrollSmooth && "scroll-smooth"}`}
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- General Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={author} />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalURL} />
|
||||
<meta property="og:image" content={socialImageURL} />
|
||||
|
||||
<!-- Article Published/Modified time -->
|
||||
{
|
||||
pubDatetime && (
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={pubDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
modDatetime && (
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={modDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={canonicalURL} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={socialImageURL} />
|
||||
|
||||
<!-- Google JSON-LD Structured data -->
|
||||
<script
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify(structuredData)}
|
||||
/>
|
||||
|
||||
<!-- Google Font -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap"
|
||||
rel="preload"
|
||||
as="style"
|
||||
onload="this.onload=null; this.rel='stylesheet';"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<meta name="theme-color" content="" />
|
||||
|
||||
{
|
||||
// If PUBLIC_GOOGLE_SITE_VERIFICATION is set in the environment variable,
|
||||
// include google-site-verification tag in the heading
|
||||
// Learn more: https://support.google.com/webmasters/answer/9008080#meta_tag_verification&zippy=%2Chtml-tag
|
||||
googleSiteVerification && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={googleSiteVerification}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<ViewTransitions />
|
||||
|
||||
<script is:inline src="/toggle-theme.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
48
src/layouts/Main.astro
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
import Breadcrumbs from "@components/Breadcrumbs.astro";
|
||||
|
||||
interface StringTitleProp {
|
||||
pageTitle: string;
|
||||
pageDesc?: string;
|
||||
}
|
||||
|
||||
interface ArrayTitleProp {
|
||||
pageTitle: [string, string];
|
||||
titleTransition: string;
|
||||
pageDesc?: string;
|
||||
}
|
||||
|
||||
export type Props = StringTitleProp | ArrayTitleProp;
|
||||
|
||||
const { props } = Astro;
|
||||
---
|
||||
|
||||
<Breadcrumbs />
|
||||
<main id="main-content">
|
||||
{
|
||||
"titleTransition" in props ? (
|
||||
<h1>
|
||||
{props.pageTitle[0]}
|
||||
<span transition:name={props.titleTransition}>
|
||||
{props.pageTitle[1]}
|
||||
</span>
|
||||
</h1>
|
||||
) : (
|
||||
<h1>{props.pageTitle}</h1>
|
||||
)
|
||||
}
|
||||
<p>{props.pageDesc}</p>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#main-content {
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-4;
|
||||
}
|
||||
#main-content h1 {
|
||||
@apply text-2xl font-semibold sm:text-3xl;
|
||||
}
|
||||
#main-content p {
|
||||
@apply mb-6 mt-2 italic;
|
||||
}
|
||||
</style>
|
313
src/layouts/PostDetails.astro
Normal file
@ -0,0 +1,313 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Tag from "@components/Tag.astro";
|
||||
import Datetime from "@components/Datetime";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { slugifyStr } from "@utils/slugify";
|
||||
import ShareLinks from "@components/ShareLinks.astro";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
posts: CollectionEntry<"blog">[];
|
||||
}
|
||||
|
||||
const { post, posts } = Astro.props;
|
||||
|
||||
const {
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
ogImage,
|
||||
canonicalURL,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
tags,
|
||||
editPost,
|
||||
} = post.data;
|
||||
|
||||
const { Content } = await post.render();
|
||||
|
||||
const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.src;
|
||||
const ogUrl = new URL(
|
||||
ogImageUrl ?? `/posts/${slugifyStr(title)}.png`,
|
||||
Astro.url.origin
|
||||
).href;
|
||||
|
||||
const layoutProps = {
|
||||
title: `${title} | ${SITE.title}`,
|
||||
author,
|
||||
description,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
canonicalURL,
|
||||
ogImage: ogUrl,
|
||||
scrollSmooth: true,
|
||||
};
|
||||
|
||||
/* ========== Prev/Next Posts ========== */
|
||||
|
||||
const allPosts = posts.map(({ data: { title }, slug }) => ({
|
||||
slug,
|
||||
title,
|
||||
}));
|
||||
|
||||
const currentPostIndex = allPosts.findIndex(a => a.slug === post.slug);
|
||||
|
||||
const prevPost = currentPostIndex !== 0 ? allPosts[currentPostIndex - 1] : null;
|
||||
const nextPost =
|
||||
currentPostIndex !== allPosts.length ? allPosts[currentPostIndex + 1] : null;
|
||||
---
|
||||
|
||||
<Layout {...layoutProps}>
|
||||
<Header />
|
||||
|
||||
<div class="mx-auto flex w-full max-w-3xl justify-start px-2">
|
||||
<button
|
||||
class="focus-outline mb-2 mt-8 flex hover:opacity-75"
|
||||
onclick="(() => (history.length === 1) ? window.location = '/' : history.back())()"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
|
||||
></path>
|
||||
</svg><span>Go back</span>
|
||||
</button>
|
||||
</div>
|
||||
<main id="main-content">
|
||||
<h1 transition:name={slugifyStr(title)} class="post-title inline-block">{title}</h1>
|
||||
<Datetime
|
||||
pubDatetime={pubDatetime}
|
||||
modDatetime={modDatetime}
|
||||
size="lg"
|
||||
className="my-2"
|
||||
editPost={editPost}
|
||||
postId={post.id}
|
||||
/>
|
||||
<article id="article" class="prose mx-auto mt-8 max-w-3xl">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<ul class="my-8">
|
||||
{tags.map(tag => <Tag tag={slugifyStr(tag)} />)}
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="flex flex-col-reverse items-center justify-between gap-6 sm:flex-row-reverse sm:items-end sm:gap-4"
|
||||
>
|
||||
<button
|
||||
id="back-to-top"
|
||||
class="focus-outline whitespace-nowrap py-1 hover:opacity-75"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="rotate-90">
|
||||
<path
|
||||
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Back to Top</span>
|
||||
</button>
|
||||
|
||||
<ShareLinks />
|
||||
</div>
|
||||
|
||||
<hr class="my-6 border-dashed" />
|
||||
|
||||
<!-- Previous/Next Post Buttons -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{
|
||||
prevPost && (
|
||||
<a
|
||||
href={`/posts/${prevPost.slug}`}
|
||||
class="flex w-full gap-1 hover:opacity-75"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left flex-none"
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M15 6l-6 6l6 6" />
|
||||
</>
|
||||
</svg>
|
||||
<div>
|
||||
<span>Previous Post</span>
|
||||
<div class="text-sm text-skin-accent/85">{prevPost.title}</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
nextPost && (
|
||||
<a
|
||||
href={`/posts/${nextPost.slug}`}
|
||||
class="flex w-full justify-end gap-1 text-right hover:opacity-75 sm:col-start-2"
|
||||
>
|
||||
<div>
|
||||
<span>Next Post</span>
|
||||
<div class="text-sm text-skin-accent/85">{nextPost.title}</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right flex-none"
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 6l6 6l-6 6" />
|
||||
</>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-12;
|
||||
}
|
||||
.post-title {
|
||||
@apply text-2xl font-semibold text-skin-accent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline data-astro-rerun>
|
||||
/** Create a progress indicator
|
||||
* at the top */
|
||||
function createProgressBar() {
|
||||
// Create the main container div
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className =
|
||||
"progress-container fixed top-0 z-10 h-1 w-full bg-skin-fill";
|
||||
|
||||
// Create the progress bar div
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.className = "progress-bar h-1 w-0 bg-skin-accent";
|
||||
progressBar.id = "myBar";
|
||||
|
||||
// Append the progress bar to the progress container
|
||||
progressContainer.appendChild(progressBar);
|
||||
|
||||
// Append the progress container to the document body or any other desired parent element
|
||||
document.body.appendChild(progressContainer);
|
||||
}
|
||||
createProgressBar();
|
||||
|
||||
/** Update the progress bar
|
||||
* when user scrolls */
|
||||
function updateScrollProgress() {
|
||||
document.addEventListener("scroll", () => {
|
||||
const winScroll =
|
||||
document.body.scrollTop || document.documentElement.scrollTop;
|
||||
const height =
|
||||
document.documentElement.scrollHeight -
|
||||
document.documentElement.clientHeight;
|
||||
const scrolled = (winScroll / height) * 100;
|
||||
if (document) {
|
||||
const myBar = document.getElementById("myBar");
|
||||
if (myBar) {
|
||||
myBar.style.width = scrolled + "%";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
updateScrollProgress();
|
||||
|
||||
/** Attaches links to headings in the document,
|
||||
* allowing sharing of sections easily */
|
||||
function addHeadingLinks() {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll("h2, h3, h4, h5, h6")
|
||||
);
|
||||
for (const heading of headings) {
|
||||
heading.classList.add("group");
|
||||
const link = document.createElement("a");
|
||||
link.className =
|
||||
"heading-link ml-2 opacity-0 group-hover:opacity-100 focus:opacity-100";
|
||||
link.href = "#" + heading.id;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.ariaHidden = "true";
|
||||
span.innerText = "#";
|
||||
link.appendChild(span);
|
||||
heading.appendChild(link);
|
||||
}
|
||||
}
|
||||
addHeadingLinks();
|
||||
|
||||
/** Attaches copy buttons to code blocks in the document,
|
||||
* allowing users to copy code easily. */
|
||||
function attachCopyButtons() {
|
||||
const copyButtonLabel = "Copy";
|
||||
const codeBlocks = Array.from(document.querySelectorAll("pre"));
|
||||
|
||||
for (const codeBlock of codeBlocks) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.style.position = "relative";
|
||||
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.className =
|
||||
"copy-code absolute right-3 -top-3 rounded bg-skin-card px-2 py-1 text-xs leading-4 text-skin-base font-medium";
|
||||
copyButton.innerHTML = copyButtonLabel;
|
||||
codeBlock.setAttribute("tabindex", "0");
|
||||
codeBlock.appendChild(copyButton);
|
||||
|
||||
// wrap codebock with relative parent element
|
||||
codeBlock?.parentNode?.insertBefore(wrapper, codeBlock);
|
||||
wrapper.appendChild(codeBlock);
|
||||
|
||||
copyButton.addEventListener("click", async () => {
|
||||
await copyCode(codeBlock, copyButton);
|
||||
});
|
||||
}
|
||||
|
||||
async function copyCode(block, button) {
|
||||
const code = block.querySelector("code");
|
||||
const text = code?.innerText;
|
||||
|
||||
await navigator.clipboard.writeText(text ?? "");
|
||||
|
||||
// visual feedback that task is completed
|
||||
button.innerText = "Copied";
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerText = copyButtonLabel;
|
||||
}, 700);
|
||||
}
|
||||
}
|
||||
attachCopyButtons();
|
||||
|
||||
/** Scrolls the document to the top when
|
||||
* the "Back to Top" button is clicked. */
|
||||
function backToTop() {
|
||||
document.querySelector("#back-to-top")?.addEventListener("click", () => {
|
||||
document.body.scrollTop = 0; // For Safari
|
||||
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
||||
});
|
||||
}
|
||||
backToTop();
|
||||
|
||||
/* Go to page start after page swap */
|
||||
document.addEventListener("astro:after-swap", () =>
|
||||
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
|
||||
);
|
||||
</script>
|
34
src/layouts/Posts.astro
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Pagination from "@components/Pagination.astro";
|
||||
import Card from "@components/Card";
|
||||
import { SITE } from "@config";
|
||||
import type { Page } from "astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
page: Page<CollectionEntry<"blog">>;
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`Posts | ${SITE.title}`}>
|
||||
<Header activeNav="posts" />
|
||||
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
|
||||
<ul>
|
||||
{
|
||||
page.data.map(({ data, slug }) => (
|
||||
<Card href={`/posts/${slug}/`} frontmatter={data} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Main>
|
||||
|
||||
<Pagination {page} />
|
||||
|
||||
<Footer noMarginTop={page.lastPage > 1} />
|
||||
</Layout>
|
41
src/layouts/TagPosts.astro
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Card from "@components/Card";
|
||||
import Pagination from "@components/Pagination.astro";
|
||||
import { SITE } from "@config";
|
||||
import type { Page } from "astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
page: Page<CollectionEntry<"blog">>;
|
||||
tag: string;
|
||||
tagName: string;
|
||||
}
|
||||
|
||||
const { page, tag, tagName } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
|
||||
<Header activeNav="tags" />
|
||||
<Main
|
||||
pageTitle={[`Tag:`, `${tagName}`]}
|
||||
titleTransition={tag}
|
||||
pageDesc={`All the articles with the tag "${tagName}".`}
|
||||
>
|
||||
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
|
||||
<ul>
|
||||
{
|
||||
page.data.map(({ data, slug }) => (
|
||||
<Card href={`/posts/${slug}/`} frontmatter={data} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Main>
|
||||
|
||||
<Pagination {page} />
|
||||
|
||||
<Footer noMarginTop={page.lastPage > 1} />
|
||||
</Layout>
|
42
src/pages/404.astro
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
import { SITE } from "@config";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
---
|
||||
|
||||
<Layout title={`404 Not Found | ${SITE.title}`}>
|
||||
<Header />
|
||||
|
||||
<main id="main-content">
|
||||
<div class="not-found-wrapper">
|
||||
<h1>404</h1>
|
||||
<span aria-hidden="true">¯\_(ツ)_/¯</span>
|
||||
<p>Page Not Found</p>
|
||||
<LinkButton
|
||||
href="/"
|
||||
className="my-6 text-lg underline decoration-dashed underline-offset-8"
|
||||
>
|
||||
Go back home
|
||||
</LinkButton>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
#main-content {
|
||||
@apply mx-auto flex max-w-3xl flex-1 items-center justify-center;
|
||||
}
|
||||
.not-found-wrapper {
|
||||
@apply mb-14 flex flex-col items-center justify-center;
|
||||
}
|
||||
.not-found-wrapper h1 {
|
||||
@apply text-9xl font-bold text-skin-accent;
|
||||
}
|
||||
.not-found-wrapper p {
|
||||
@apply mt-4 text-2xl sm:text-3xl;
|
||||
}
|
||||
</style>
|
36
src/pages/about.md
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
layout: ../layouts/AboutLayout.astro
|
||||
title: "About"
|
||||
---
|
||||
|
||||
AstroPaper is a minimal, responsive and SEO-friendly Astro blog theme. I designed and crafted this based on [my personal blog](https://satnaing.dev/blog).
|
||||
|
||||
This theme is aimed to be accessible out of the box. Light and dark mode are supported by
|
||||
default and additional color schemes can also be configured.
|
||||
|
||||
This theme is self-documented \_ which means articles/posts in this theme can also be considered as documentations. So, see the documentation for more info.
|
||||
|
||||
<div>
|
||||
<img src="/assets/dev.svg" class="sm:w-1/2 mx-auto" alt="coding dev illustration">
|
||||
</div>
|
||||
|
||||
## Tech Stack
|
||||
|
||||
This theme is written in vanilla JavaScript (+ TypeScript for type checking) and a little bit of ReactJS for some interactions. TailwindCSS is used for styling; and Markdown is used for blog contents.
|
||||
|
||||
## Features
|
||||
|
||||
Here are certain features of this site.
|
||||
|
||||
- fully responsive and accessible
|
||||
- SEO-friendly
|
||||
- light & dark mode
|
||||
- fuzzy search
|
||||
- super fast performance
|
||||
- draft posts
|
||||
- pagination
|
||||
- sitemap & rss feed
|
||||
- highly customizable
|
||||
|
||||
If you like this theme, you can star/contribute to the [repo](https://github.com/satnaing/astro-paper).
|
||||
Or you can even give any feedback via my [email](mailto:contact@satnaing.dev).
|
84
src/pages/archives/index.astro
Normal file
@ -0,0 +1,84 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Card from "@components/Card";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import { SITE } from "@config";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import getPostsByGroupCondition from "@utils/getPostsByGroupCondition";
|
||||
|
||||
// Redirect to 404 page if `showArchives` config is false
|
||||
if (!SITE.showArchives) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const MonthMap: Record<string, string> = {
|
||||
"1": "January",
|
||||
"2": "February",
|
||||
"3": "March",
|
||||
"4": "April",
|
||||
"5": "May",
|
||||
"6": "June",
|
||||
"7": "July",
|
||||
"8": "August",
|
||||
"9": "September",
|
||||
"10": "October",
|
||||
"11": "November",
|
||||
"12": "December",
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={`Archives | ${SITE.title}`}>
|
||||
<Header activeNav="archives" />
|
||||
<Main pageTitle="Archives" pageDesc="All the articles I've archived.">
|
||||
{
|
||||
Object.entries(
|
||||
getPostsByGroupCondition(posts, post =>
|
||||
post.data.pubDatetime.getFullYear()
|
||||
)
|
||||
)
|
||||
.sort(([yearA], [yearB]) => Number(yearB) - Number(yearA))
|
||||
.map(([year, yearGroup]) => (
|
||||
<div>
|
||||
<span class="text-2xl font-bold">{year}</span>
|
||||
<sup class="text-sm">{yearGroup.length}</sup>
|
||||
{Object.entries(
|
||||
getPostsByGroupCondition(
|
||||
yearGroup,
|
||||
post => post.data.pubDatetime.getMonth() + 1
|
||||
)
|
||||
)
|
||||
.sort(([monthA], [monthB]) => Number(monthB) - Number(monthA))
|
||||
.map(([month, monthGroup]) => (
|
||||
<div class="flex flex-col sm:flex-row">
|
||||
<div class="mt-6 min-w-36 text-lg sm:my-6">
|
||||
<span class="font-bold">{MonthMap[month]}</span>
|
||||
<sup class="text-xs">{monthGroup.length}</sup>
|
||||
</div>
|
||||
<ul>
|
||||
{monthGroup
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.floor(
|
||||
new Date(b.data.pubDatetime).getTime() / 1000
|
||||
) -
|
||||
Math.floor(
|
||||
new Date(a.data.pubDatetime).getTime() / 1000
|
||||
)
|
||||
)
|
||||
.map(({ data, slug }) => (
|
||||
<Card href={`/posts/${slug}`} frontmatter={data} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
116
src/pages/index.astro
Normal file
@ -0,0 +1,116 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
import Hr from "@components/Hr.astro";
|
||||
import Card from "@components/Card";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
import { SITE } from "@config";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
|
||||
const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
|
||||
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Header />
|
||||
<main id="main-content">
|
||||
|
||||
|
||||
{
|
||||
featuredPosts.length > 0 && (
|
||||
<>
|
||||
<section id="featured">
|
||||
<h2>Featured</h2>
|
||||
<ul>
|
||||
{featuredPosts.map(({ data, slug }) => (
|
||||
<Card
|
||||
href={`/posts/${slug}/`}
|
||||
frontmatter={data}
|
||||
secHeading={false}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
{recentPosts.length > 0 && <Hr />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
recentPosts.length > 0 && (
|
||||
<section id="recent-posts">
|
||||
<h2>Recent Posts</h2>
|
||||
<ul>
|
||||
{recentPosts.map(
|
||||
({ data, slug }, index) =>
|
||||
index < SITE.postPerIndex && (
|
||||
<Card
|
||||
href={`/posts/${slug}/`}
|
||||
frontmatter={data}
|
||||
secHeading={false}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="all-posts-btn-wrapper">
|
||||
<LinkButton href="/posts/">
|
||||
All Posts
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z"
|
||||
></path>
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* ===== Hero Section ===== */
|
||||
#hero {
|
||||
@apply pb-6 pt-8;
|
||||
}
|
||||
#hero h1 {
|
||||
@apply my-4 inline-block text-3xl font-bold sm:my-8 sm:text-5xl;
|
||||
}
|
||||
#hero .rss-link {
|
||||
@apply mb-6;
|
||||
}
|
||||
#hero .rss-icon {
|
||||
@apply mb-2 h-6 w-6 scale-110 fill-skin-accent sm:mb-3 sm:scale-125;
|
||||
}
|
||||
#hero p {
|
||||
@apply my-2;
|
||||
}
|
||||
.social-wrapper {
|
||||
@apply mt-4 flex flex-col sm:flex-row sm:items-center;
|
||||
}
|
||||
.social-links {
|
||||
@apply mb-1 mr-2 whitespace-nowrap sm:mb-0;
|
||||
}
|
||||
|
||||
/* ===== Featured & Recent Posts Sections ===== */
|
||||
#featured,
|
||||
#recent-posts {
|
||||
@apply pb-6 pt-12;
|
||||
}
|
||||
#featured h2,
|
||||
#recent-posts h2 {
|
||||
@apply text-2xl font-semibold tracking-wide;
|
||||
}
|
||||
.all-posts-btn-wrapper {
|
||||
@apply my-8 text-center;
|
||||
}
|
||||
</style>
|
7
src/pages/og.png.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { generateOgImageForSite } from "@utils/generateOgImages";
|
||||
|
||||
export const GET: APIRoute = async () =>
|
||||
new Response(await generateOgImageForSite(), {
|
||||
headers: { "Content-Type": "image/png" },
|
||||
});
|
16
src/pages/posts/[...page].astro
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
import { SITE } from "@config";
|
||||
import Posts from "@layouts/Posts.astro";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
return paginate(getSortedPosts(posts), { pageSize: SITE.postPerPage });
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Posts {page} />
|
27
src/pages/posts/[slug]/index.astro
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import PostDetails from "@layouts/PostDetails.astro";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const postResult = posts.map(post => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
|
||||
return postResult;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
---
|
||||
|
||||
<PostDetails post={post} posts={sortedPosts} />
|
20
src/pages/posts/[slug]/index.png.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getCollection, type CollectionEntry } from "astro:content";
|
||||
import { generateOgImageForPost } from "@utils/generateOgImages";
|
||||
import { slugifyStr } from "@utils/slugify";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog").then(p =>
|
||||
p.filter(({ data }) => !data.draft && !data.ogImage)
|
||||
);
|
||||
|
||||
return posts.map(post => ({
|
||||
params: { slug: slugifyStr(post.data.title) },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ props }) =>
|
||||
new Response(await generateOgImageForPost(props as CollectionEntry<"blog">), {
|
||||
headers: { "Content-Type": "image/png" },
|
||||
});
|
17
src/pages/robots.txt.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { SITE } from "@config";
|
||||
|
||||
const robots = `
|
||||
User-agent: Googlebot
|
||||
Disallow: /nogooglebot/
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${new URL("sitemap-index.xml", SITE.website).href}
|
||||
`.trim();
|
||||
|
||||
export const GET: APIRoute = () =>
|
||||
new Response(robots, {
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
});
|
20
src/pages/rss.xml.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export async function GET() {
|
||||
const posts = await getCollection("blog");
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
return rss({
|
||||
title: SITE.title,
|
||||
description: SITE.desc,
|
||||
site: SITE.website,
|
||||
items: sortedPosts.map(({ data, slug }) => ({
|
||||
link: `posts/${slug}/`,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
pubDate: new Date(data.modDatetime ?? data.pubDatetime),
|
||||
})),
|
||||
});
|
||||
}
|
30
src/pages/search.astro
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import { SITE } from "@config";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import SearchBar from "@components/Search";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
|
||||
// Retrieve all published articles
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
|
||||
// List of items to search in
|
||||
const searchList = sortedPosts.map(({ data, slug }) => ({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
data,
|
||||
slug,
|
||||
}));
|
||||
---
|
||||
|
||||
<Layout title={`Search | ${SITE.title}`}>
|
||||
<Header activeNav="search" />
|
||||
<Main pageTitle="Search" pageDesc="Search any article ...">
|
||||
<SearchBar client:load searchList={searchList} />
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
29
src/pages/tags/[tag]/[...page].astro
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import TagPosts from "@layouts/TagPosts.astro";
|
||||
import getUniqueTags from "@utils/getUniqueTags";
|
||||
import getPostsByTag from "@utils/getPostsByTag";
|
||||
import type { GetStaticPathsOptions } from "astro";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
||||
const posts = await getCollection("blog");
|
||||
const tags = getUniqueTags(posts);
|
||||
|
||||
return tags.flatMap(({ tag, tagName }) => {
|
||||
const tagPosts = getPostsByTag(posts, tag);
|
||||
|
||||
return paginate(tagPosts, {
|
||||
params: { tag },
|
||||
props: { tagName },
|
||||
pageSize: SITE.postPerPage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const params = Astro.params;
|
||||
const { tag } = params;
|
||||
const { page, tagName } = Astro.props;
|
||||
---
|
||||
|
||||
<TagPosts {page} {tag} {tagName} />
|
24
src/pages/tags/index.astro
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Tag from "@components/Tag.astro";
|
||||
import getUniqueTags from "@utils/getUniqueTags";
|
||||
import { SITE } from "@config";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
let tags = getUniqueTags(posts);
|
||||
---
|
||||
|
||||
<Layout title={`Tags | ${SITE.title}`}>
|
||||
<Header activeNav="tags" />
|
||||
<Main pageTitle="Tags" pageDesc="All the tags used in posts.">
|
||||
<ul>
|
||||
{tags.map(({ tag }) => <Tag {tag} size="lg" />)}
|
||||
</ul>
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
128
src/styles/base.css
Normal file
@ -0,0 +1,128 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root,
|
||||
html[data-theme="light"] {
|
||||
--color-fill: 243, 243, 243; /* Light gray background */
|
||||
--color-text-base: 51, 51, 51; /* Dark gray text */
|
||||
--color-accent: 208, 20, 114; /* Vibrant pink primary */
|
||||
--color-card: 230, 230, 230; /* Off-white cards */
|
||||
--color-card-muted: 205, 205, 205; /* Medium gray muted elements */
|
||||
--color-border: 220, 220, 220; /* Light gray borders */
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--color-fill: 61, 10, 35; /* Deep burgundy background */
|
||||
--color-text-base: 234, 237, 243; /* Light gray text */
|
||||
--color-accent: 208, 20, 114; /* Consistent pink accent */
|
||||
--color-card: 82, 20, 50; /* Rich plum cards */
|
||||
--color-card-muted: 41, 5, 23; /* Dark burgundy muted elements */
|
||||
--color-border: 102, 20, 60; /* Medium plum borders */
|
||||
}
|
||||
#sun-svg,
|
||||
html[data-theme="dark"] #moon-svg {
|
||||
display: none;
|
||||
}
|
||||
#moon-svg,
|
||||
html[data-theme="dark"] #sun-svg {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
@apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base selection:bg-skin-accent/70 selection:text-skin-inverted;
|
||||
}
|
||||
section,
|
||||
footer {
|
||||
@apply mx-auto max-w-3xl px-4;
|
||||
}
|
||||
a {
|
||||
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
|
||||
}
|
||||
svg {
|
||||
@apply inline-block h-6 w-6 fill-skin-base group-hover:fill-skin-accent;
|
||||
}
|
||||
svg.icon-tabler {
|
||||
@apply inline-block h-6 w-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110;
|
||||
}
|
||||
.prose {
|
||||
@apply prose-headings:!mb-3 prose-headings:!text-skin-base prose-h3:italic prose-p:!text-skin-base prose-a:!text-skin-base prose-a:!decoration-dashed prose-a:underline-offset-8 hover:prose-a:text-skin-accent prose-blockquote:!border-l-skin-accent/50 prose-blockquote:opacity-80 prose-figcaption:!text-skin-base prose-figcaption:opacity-70 prose-strong:!text-skin-base prose-code:rounded prose-code:bg-skin-card/75 prose-code:p-1 prose-code:before:!content-none prose-code:after:!content-none prose-ol:!text-skin-base prose-ul:overflow-x-clip prose-ul:!text-skin-base prose-li:marker:!text-skin-accent prose-table:text-skin-base prose-th:border prose-th:border-skin-line prose-td:border prose-td:border-skin-line prose-img:!my-2 prose-img:mx-auto prose-img:border-2 prose-img:border-skin-line prose-hr:!border-skin-line;
|
||||
}
|
||||
.prose a {
|
||||
@apply break-words hover:!text-skin-accent;
|
||||
}
|
||||
.prose thead th:first-child,
|
||||
tbody td:first-child,
|
||||
tfoot td:first-child {
|
||||
padding-left: 0.5714286em;
|
||||
}
|
||||
.prose h2#table-of-contents {
|
||||
@apply mb-2;
|
||||
}
|
||||
.prose details {
|
||||
@apply inline-block cursor-pointer select-none text-skin-base;
|
||||
}
|
||||
.prose summary {
|
||||
@apply focus-outline;
|
||||
}
|
||||
.prose h2#table-of-contents + p {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
/* ===== scrollbar ===== */
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
@apply w-3;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-skin-fill;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-skin-card;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-skin-card-muted;
|
||||
}
|
||||
|
||||
/* ===== Code Blocks & Syntax Highlighting ===== */
|
||||
pre:has(code) {
|
||||
@apply border border-skin-line;
|
||||
}
|
||||
code,
|
||||
blockquote {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Apply Dark Theme (if multi-theme specified) */
|
||||
html[data-theme="dark"] pre:has(code),
|
||||
html[data-theme="dark"] pre:has(code) span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.display-none {
|
||||
@apply hidden;
|
||||
}
|
||||
.focus-outline {
|
||||
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
|
||||
}
|
||||
}
|
27
src/types.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type socialIcons from "@assets/socialIcons";
|
||||
|
||||
export type Site = {
|
||||
website: string;
|
||||
author: string;
|
||||
profile: string;
|
||||
desc: string;
|
||||
title: string;
|
||||
ogImage?: string;
|
||||
lightAndDarkMode: boolean;
|
||||
postPerIndex: number;
|
||||
postPerPage: number;
|
||||
scheduledPostMargin: number;
|
||||
showArchives?: boolean;
|
||||
editPost?: {
|
||||
url?: URL["href"];
|
||||
text?: string;
|
||||
appendFilePath?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type SocialObjects = {
|
||||
name: keyof typeof socialIcons;
|
||||
href: string;
|
||||
active: boolean;
|
||||
linkTitle: string;
|
||||
}[];
|
20
src/utils/generateOgImages.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Resvg } from "@resvg/resvg-js";
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import postOgImage from "./og-templates/post";
|
||||
import siteOgImage from "./og-templates/site";
|
||||
|
||||
function svgBufferToPngBuffer(svg: string) {
|
||||
const resvg = new Resvg(svg);
|
||||
const pngData = resvg.render();
|
||||
return pngData.asPng();
|
||||
}
|
||||
|
||||
export async function generateOgImageForPost(post: CollectionEntry<"blog">) {
|
||||
const svg = await postOgImage(post);
|
||||
return svgBufferToPngBuffer(svg);
|
||||
}
|
||||
|
||||
export async function generateOgImageForSite() {
|
||||
const svg = await siteOgImage();
|
||||
return svgBufferToPngBuffer(svg);
|
||||
}
|
25
src/utils/getPostsByGroupCondition.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
type GroupKey = string | number | symbol;
|
||||
|
||||
interface GroupFunction<T> {
|
||||
(item: T, index?: number): GroupKey;
|
||||
}
|
||||
|
||||
const getPostsByGroupCondition = (
|
||||
posts: CollectionEntry<"blog">[],
|
||||
groupFunction: GroupFunction<CollectionEntry<"blog">>
|
||||
) => {
|
||||
const result: Record<GroupKey, CollectionEntry<"blog">[]> = {};
|
||||
for (let i = 0; i < posts.length; i++) {
|
||||
const item = posts[i];
|
||||
const groupKey = groupFunction(item, i);
|
||||
if (!result[groupKey]) {
|
||||
result[groupKey] = [];
|
||||
}
|
||||
result[groupKey].push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default getPostsByGroupCondition;
|
10
src/utils/getPostsByTag.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import getSortedPosts from "./getSortedPosts";
|
||||
import { slugifyAll } from "./slugify";
|
||||
|
||||
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
|
||||
getSortedPosts(
|
||||
posts.filter(post => slugifyAll(post.data.tags).includes(tag))
|
||||
);
|
||||
|
||||
export default getPostsByTag;
|
18
src/utils/getSortedPosts.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import postFilter from "./postFilter";
|
||||
|
||||
const getSortedPosts = (posts: CollectionEntry<"blog">[]) => {
|
||||
return posts
|
||||
.filter(postFilter)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.floor(
|
||||
new Date(b.data.modDatetime ?? b.data.pubDatetime).getTime() / 1000
|
||||
) -
|
||||
Math.floor(
|
||||
new Date(a.data.modDatetime ?? a.data.pubDatetime).getTime() / 1000
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default getSortedPosts;
|
23
src/utils/getUniqueTags.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { slugifyStr } from "./slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import postFilter from "./postFilter";
|
||||
|
||||
interface Tag {
|
||||
tag: string;
|
||||
tagName: string;
|
||||
}
|
||||
|
||||
const getUniqueTags = (posts: CollectionEntry<"blog">[]) => {
|
||||
const tags: Tag[] = posts
|
||||
.filter(postFilter)
|
||||
.flatMap(post => post.data.tags)
|
||||
.map(tag => ({ tag: slugifyStr(tag), tagName: tag }))
|
||||
.filter(
|
||||
(value, index, self) =>
|
||||
self.findIndex(tag => tag.tag === value.tag) === index
|
||||
)
|
||||
.sort((tagA, tagB) => tagA.tag.localeCompare(tagB.tag));
|
||||
return tags;
|
||||
};
|
||||
|
||||
export default getUniqueTags;
|
71
src/utils/loadGoogleFont.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type { FontStyle, FontWeight } from "satori";
|
||||
|
||||
export type FontOptions = {
|
||||
name: string;
|
||||
data: ArrayBuffer;
|
||||
weight: FontWeight | undefined;
|
||||
style: FontStyle | undefined;
|
||||
};
|
||||
|
||||
async function loadGoogleFont(
|
||||
font: string,
|
||||
text: string
|
||||
): Promise<ArrayBuffer> {
|
||||
const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
|
||||
|
||||
const css = await (
|
||||
await fetch(API, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
|
||||
},
|
||||
})
|
||||
).text();
|
||||
|
||||
const resource = css.match(
|
||||
/src: url\((.+)\) format\('(opentype|truetype)'\)/
|
||||
);
|
||||
|
||||
if (!resource) throw new Error("Failed to download dynamic font");
|
||||
|
||||
const res = await fetch(resource[1]);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to download dynamic font. Status: " + res.status);
|
||||
}
|
||||
|
||||
const fonts: ArrayBuffer = await res.arrayBuffer();
|
||||
return fonts;
|
||||
}
|
||||
|
||||
async function loadGoogleFonts(
|
||||
text: string
|
||||
): Promise<
|
||||
Array<{ name: string; data: ArrayBuffer; weight: number; style: string }>
|
||||
> {
|
||||
const fontsConfig = [
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
font: "IBM+Plex+Mono",
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
font: "IBM+Plex+Mono:wght@700",
|
||||
weight: 700,
|
||||
style: "bold",
|
||||
},
|
||||
];
|
||||
|
||||
const fonts = await Promise.all(
|
||||
fontsConfig.map(async ({ name, font, weight, style }) => {
|
||||
const data = await loadGoogleFont(font, text);
|
||||
return { name, data, weight, style };
|
||||
})
|
||||
);
|
||||
|
||||
return fonts;
|
||||
}
|
||||
|
||||
export default loadGoogleFonts;
|
106
src/utils/og-templates/post.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import satori from "satori";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { SITE } from "@config";
|
||||
import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont";
|
||||
|
||||
export default async (post: CollectionEntry<"blog">) => {
|
||||
return satori(
|
||||
<div
|
||||
style={{
|
||||
background: "#fefbfb",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: "-1px",
|
||||
border: "4px solid #000",
|
||||
background: "#ecebeb",
|
||||
opacity: "0.9",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "2.5rem",
|
||||
width: "88%",
|
||||
height: "80%",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
border: "4px solid #000",
|
||||
background: "#fefbfb",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "2rem",
|
||||
width: "88%",
|
||||
height: "80%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
margin: "20px",
|
||||
width: "90%",
|
||||
height: "90%",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: "bold",
|
||||
maxHeight: "84%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{post.data.title}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
marginBottom: "8px",
|
||||
fontSize: 28,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
by{" "}
|
||||
<span
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
"
|
||||
</span>
|
||||
<span style={{ overflow: "hidden", fontWeight: "bold" }}>
|
||||
{post.data.author}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span style={{ overflow: "hidden", fontWeight: "bold" }}>
|
||||
{SITE.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
embedFont: true,
|
||||
fonts: (await loadGoogleFonts(
|
||||
post.data.title + post.data.author + SITE.title + "by"
|
||||
)) as FontOptions[],
|
||||
}
|
||||
);
|
||||
};
|
97
src/utils/og-templates/site.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import satori from "satori";
|
||||
import { SITE } from "@config";
|
||||
import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont";
|
||||
|
||||
export default async () => {
|
||||
return satori(
|
||||
<div
|
||||
style={{
|
||||
background: "#fefbfb",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: "-1px",
|
||||
border: "4px solid #000",
|
||||
background: "#ecebeb",
|
||||
opacity: "0.9",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "2.5rem",
|
||||
width: "88%",
|
||||
height: "80%",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
border: "4px solid #000",
|
||||
background: "#fefbfb",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "2rem",
|
||||
width: "88%",
|
||||
height: "80%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
margin: "20px",
|
||||
width: "90%",
|
||||
height: "90%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "90%",
|
||||
maxHeight: "90%",
|
||||
overflow: "hidden",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: 72, fontWeight: "bold" }}>{SITE.title}</p>
|
||||
<p style={{ fontSize: 28 }}>{SITE.desc}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
width: "100%",
|
||||
marginBottom: "8px",
|
||||
fontSize: 28,
|
||||
}}
|
||||
>
|
||||
<span style={{ overflow: "hidden", fontWeight: "bold" }}>
|
||||
{new URL(SITE.website).hostname}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
embedFont: true,
|
||||
fonts: (await loadGoogleFonts(
|
||||
SITE.title + SITE.desc + SITE.website
|
||||
)) as FontOptions[],
|
||||
}
|
||||
);
|
||||
};
|
11
src/utils/postFilter.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { SITE } from "@config";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
const postFilter = ({ data }: CollectionEntry<"blog">) => {
|
||||
const isPublishTimePassed =
|
||||
Date.now() >
|
||||
new Date(data.pubDatetime).getTime() - SITE.scheduledPostMargin;
|
||||
return !data.draft && (import.meta.env.DEV || isPublishTimePassed);
|
||||
};
|
||||
|
||||
export default postFilter;
|
5
src/utils/slugify.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import kebabcase from "lodash.kebabcase";
|
||||
|
||||
export const slugifyStr = (str: string) => kebabcase(str);
|
||||
|
||||
export const slugifyAll = (arr: string[]) => arr.map(str => slugifyStr(str));
|
81
tailwind.config.cjs
Normal file
@ -0,0 +1,81 @@
|
||||
function withOpacity(variableName) {
|
||||
return ({ opacityValue }) => {
|
||||
if (opacityValue !== undefined) {
|
||||
return `rgba(var(${variableName}), ${opacityValue})`;
|
||||
}
|
||||
return `rgb(var(${variableName}))`;
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["selector", "[data-theme='dark']"],
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
theme: {
|
||||
// Remove the following screen breakpoint or add other breakpoints
|
||||
// if one breakpoint is not enough for you
|
||||
screens: {
|
||||
sm: "640px",
|
||||
},
|
||||
|
||||
extend: {
|
||||
textColor: {
|
||||
skin: {
|
||||
base: withOpacity("--color-text-base"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
inverted: withOpacity("--color-fill"),
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
skin: {
|
||||
fill: withOpacity("--color-fill"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
inverted: withOpacity("--color-text-base"),
|
||||
card: withOpacity("--color-card"),
|
||||
"card-muted": withOpacity("--color-card-muted"),
|
||||
},
|
||||
},
|
||||
outlineColor: {
|
||||
skin: {
|
||||
fill: withOpacity("--color-accent"),
|
||||
},
|
||||
},
|
||||
borderColor: {
|
||||
skin: {
|
||||
line: withOpacity("--color-border"),
|
||||
fill: withOpacity("--color-text-base"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
skin: {
|
||||
base: withOpacity("--color-text-base"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
},
|
||||
transparent: "transparent",
|
||||
},
|
||||
stroke: {
|
||||
skin: {
|
||||
accent: withOpacity("--color-accent")
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["IBM Plex Mono", "monospace"],
|
||||
},
|
||||
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
pre: {
|
||||
color: false,
|
||||
},
|
||||
code: {
|
||||
color: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@assets/*": ["assets/*"],
|
||||
"@config": ["config.ts"],
|
||||
"@components/*": ["components/*"],
|
||||
"@content/*": ["content/*"],
|
||||
"@layouts/*": ["layouts/*"],
|
||||
"@pages/*": ["pages/*"],
|
||||
"@styles/*": ["styles/*"],
|
||||
"@utils/*": ["utils/*"]
|
||||
}
|
||||
}
|
||||
}
|