First release
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = false
|
41
.github/workflows/actions.yaml
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: GitHub Actions
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version:
|
||||||
|
- 18
|
||||||
|
- 20
|
||||||
|
- 22
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js v${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
# - run: npm test
|
||||||
|
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js 22
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run check
|
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
.astro
|
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Expose Astro dependencies for `pnpm` users
|
||||||
|
shamefully-hoist=true
|
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.github
|
||||||
|
.changeset
|
13
.prettierrc.cjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('prettier').Config} */
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 120,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
useTabs: false,
|
||||||
|
|
||||||
|
plugins: [require.resolve('prettier-plugin-astro')],
|
||||||
|
|
||||||
|
overrides: [{ files: '*.astro', options: { parser: 'astro' } }],
|
||||||
|
};
|
6
.stackblitzrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"startCommand": "npm start",
|
||||||
|
"env": {
|
||||||
|
"ENABLE_CJS_IMPORTS": true
|
||||||
|
}
|
||||||
|
}
|
275
.vscode/astrowind/config-schema.json
vendored
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"site": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"trailingSlash": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"googleSiteVerificationId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "site", "base", "trailingSlash"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["default", "template"]
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index", "follow"]
|
||||||
|
},
|
||||||
|
"openGraph": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"site_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["url", "width", "height"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["site_name", "images", "type"]
|
||||||
|
},
|
||||||
|
"twitter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"handle": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cardType": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["handle", "site", "cardType"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "description", "robots", "openGraph", "twitter"]
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"language": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"textDirection": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["language", "textDirection"]
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"blog": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"postsPerPage": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"isRelatedPostsEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"relatedPostsCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"permalink": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "permalink", "robots"]
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "pathname", "robots"]
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "pathname", "robots"]
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "pathname", "robots"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "postsPerPage", "post", "list", "category", "tag"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["blog"]
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vendors": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"googleAnalytics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": ["string", "null"]
|
||||||
|
},
|
||||||
|
"partytown": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["googleAnalytics"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["vendors"]
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"theme": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["theme"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["site", "metadata", "i18n", "apps", "analytics", "ui"]
|
||||||
|
}
|
10
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"astro-build.astro-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"unifiedjs.vscode-mdx"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
15
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"css.customData": ["./vscode.tailwind.json"],
|
||||||
|
"eslint.validate": ["javascript", "javascriptreact", "astro", "typescript", "typescriptreact"],
|
||||||
|
"files.associations": {
|
||||||
|
"*.mdx": "markdown"
|
||||||
|
},
|
||||||
|
"prettier.documentSelectors": ["**/*.astro"],
|
||||||
|
"[astro]": {
|
||||||
|
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||||
|
},
|
||||||
|
"yaml.schemas": {
|
||||||
|
"./.vscode/astrowind/config-schema.json": "/src/config.yaml"
|
||||||
|
},
|
||||||
|
"eslint.useFlatConfig": true
|
||||||
|
}
|
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 onWidget
|
||||||
|
|
||||||
|
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.
|
295
README.md
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
# 🚀 AstroWind
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/onwidget/.github/main/resources/astrowind/lighthouse-score.png" align="right"
|
||||||
|
alt="AstroWind Lighthouse Score" width="100" height="358">
|
||||||
|
|
||||||
|
🌟 _Most *starred* & *forked* Astro theme in 2022 & 2023_. 🌟
|
||||||
|
|
||||||
|
**AstroWind** is a free and open-source template to make your website using **[Astro 4.0](https://astro.build/) + [Tailwind CSS](https://tailwindcss.com/)**. Ready to start a new project and designed taking into account web best practices.
|
||||||
|
|
||||||
|
- ✅ **Production-ready** scores in **PageSpeed Insights** reports.
|
||||||
|
- ✅ Integration with **Tailwind CSS** supporting **Dark mode** and **_RTL_**.
|
||||||
|
- ✅ **Fast and SEO friendly blog** with automatic **RSS feed**, **MDX** support, **Categories & Tags**, **Social Share**, ...
|
||||||
|
- ✅ **Image Optimization** (using new **Astro Assets** and **Unpic** for Universal image CDN).
|
||||||
|
- ✅ Generation of **project sitemap** based on your routes.
|
||||||
|
- ✅ **Open Graph tags** for social media sharing.
|
||||||
|
- ✅ **Analytics** built-in Google Analytics, and Splitbee integration.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/onwidget/.github/main/resources/astrowind/screenshot-astrowind-1.png" alt="AstroWind Theme Screenshot">
|
||||||
|
|
||||||
|
[![onWidget](https://custom-icon-badges.demolab.com/badge/made%20by%20-onWidget-556bf2?style=flat-square&logo=onwidget&logoColor=white&labelColor=101827)](https://onwidget.com)
|
||||||
|
[![License](https://img.shields.io/github/license/onwidget/astrowind?style=flat-square&color=dddddd&labelColor=000000)](https://github.com/onwidget/astrowind/blob/main/LICENSE.md)
|
||||||
|
[![Maintained](https://img.shields.io/badge/maintained%3F-yes-brightgreen.svg?style=flat-square)](https://github.com/onwidget)
|
||||||
|
[![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/onwidget/astrowind#contributing)
|
||||||
|
[![Known Vulnerabilities](https://snyk.io/test/github/onwidget/astrowind/badge.svg?style=flat-square)](https://snyk.io/test/github/onwidget/astrowind)
|
||||||
|
[![Stars](https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=stars&maxAge=86400&color=ff69b4)](https://github.com/onwidget/astrowind)
|
||||||
|
[![Forks](https://img.shields.io/github/forks/onwidget/astrowind.svg?style=social&label=forks&maxAge=86400&color=ff69b4)](https://github.com/onwidget/astrowind)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>Table of Contents</summary>
|
||||||
|
|
||||||
|
- [Demo](#demo)
|
||||||
|
- [Upcoming: AstroWind 2.0 – We Need Your Vision!](#-upcoming-astrowind-20--we-need-your-vision)
|
||||||
|
- [Getting started](#getting-started)
|
||||||
|
- [Project structure](#project-structure)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Deploy](#deploy)
|
||||||
|
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||||
|
- [Related Projects](#related-projects)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Acknowledgements](#acknowledgements)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
📌 [https://astrowind.vercel.app/](https://astrowind.vercel.app/)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 🔔 Upcoming: AstroWind 2.0 – We Need Your Vision!
|
||||||
|
|
||||||
|
We're embarking on an exciting journey with **AstroWind 2.0**, and we want you to be a part of it! We're currently taking the first steps in developing this new version and your insights are invaluable. Join the discussion and share your feedback, ideas, and suggestions to help shape the future of **AstroWind**. Let's make **AstroWind 2.0** even better, together!
|
||||||
|
|
||||||
|
[Share Your Feedback in Our Discussion!](https://github.com/onwidget/astrowind/discussions/392)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
**AstroWind** tries to give you quick access to creating a website using [Astro 4.0](https://astro.build/) + [Tailwind CSS](https://tailwindcss.com/). It's a free theme which focuses on simplicity, good practices and high performance.
|
||||||
|
|
||||||
|
Very little vanilla javascript is used only to provide basic functionality so that each developer decides which framework (React, Vue, Svelte, Solid JS...) to use and how to approach their goals.
|
||||||
|
|
||||||
|
In this version the template supports all the options in the `output` configuration, `static`, `hybrid` and `server`, but the blog only works with `prerender = true`. We are working on the next version and aim to make it fully compatible with SSR.
|
||||||
|
|
||||||
|
### Project structure
|
||||||
|
|
||||||
|
Inside **AstroWind** template, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
│ ├── _headers
|
||||||
|
│ └── robots.txt
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── favicons/
|
||||||
|
│ │ ├── images/
|
||||||
|
│ │ └── styles/
|
||||||
|
│ │ └── tailwind.css
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── blog/
|
||||||
|
│ │ ├── common/
|
||||||
|
│ │ ├── ui/
|
||||||
|
│ │ ├── widgets/
|
||||||
|
│ │ │ ├── Header.astro
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── CustomStyles.astro
|
||||||
|
│ │ ├── Favicons.astro
|
||||||
|
│ │ └── Logo.astro
|
||||||
|
│ ├── content/
|
||||||
|
│ │ ├── post/
|
||||||
|
│ │ │ ├── post-slug-1.md
|
||||||
|
│ │ │ ├── post-slug-2.mdx
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └-- config.ts
|
||||||
|
│ ├── layouts/
|
||||||
|
│ │ ├── Layout.astro
|
||||||
|
│ │ ├── MarkdownLayout.astro
|
||||||
|
│ │ └── PageLayout.astro
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ ├── [...blog]/
|
||||||
|
│ │ │ ├── [category]/
|
||||||
|
│ │ │ ├── [tag]/
|
||||||
|
│ │ │ ├── [...page].astro
|
||||||
|
│ │ │ └── index.astro
|
||||||
|
│ │ ├── index.astro
|
||||||
|
│ │ ├── 404.astro
|
||||||
|
│ │ ├-- rss.xml.ts
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── utils/
|
||||||
|
│ ├── config.yaml
|
||||||
|
│ └── navigation.js
|
||||||
|
├── package.json
|
||||||
|
├── astro.config.ts
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory if they do not require any transformation or in the `assets/` directory if they are imported directly.
|
||||||
|
|
||||||
|
[![Edit AstroWind on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://githubbox.com/onwidget/astrowind/tree/main) [![Open in Gitpod](https://svgshare.com/i/xdi.svg)](https://gitpod.io/?on=gitpod#https://github.com/onwidget/astrowind) [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/onwidget/astrowind)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file `README.md`. Update `src/config.yaml` and contents. Have fun!
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------ | :------------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run check` | Check your project for errors |
|
||||||
|
| `npm run fix` | Run Eslint and format codes with Prettier |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Basic configuration file: `./src/config.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
site:
|
||||||
|
name: 'Example'
|
||||||
|
site: 'https://example.com'
|
||||||
|
base: '/' # Change this if you need to deploy to Github Pages, for example
|
||||||
|
trailingSlash: false # Generate permalinks with or without "/" at the end
|
||||||
|
|
||||||
|
googleSiteVerificationId: false # Or some value,
|
||||||
|
|
||||||
|
# Default SEO metadata
|
||||||
|
metadata:
|
||||||
|
title:
|
||||||
|
default: 'Example'
|
||||||
|
template: '%s — Example'
|
||||||
|
description: 'This is the default meta description of Example website'
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
follow: true
|
||||||
|
openGraph:
|
||||||
|
site_name: 'Example'
|
||||||
|
images:
|
||||||
|
- url: '~/assets/images/default.png'
|
||||||
|
width: 1200
|
||||||
|
height: 628
|
||||||
|
type: website
|
||||||
|
twitter:
|
||||||
|
handle: '@twitter_user'
|
||||||
|
site: '@twitter_user'
|
||||||
|
cardType: summary_large_image
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
language: en
|
||||||
|
textDirection: ltr
|
||||||
|
|
||||||
|
apps:
|
||||||
|
blog:
|
||||||
|
isEnabled: true # If the blog will be enabled
|
||||||
|
postsPerPage: 6 # Number of posts per page
|
||||||
|
|
||||||
|
post:
|
||||||
|
isEnabled: true
|
||||||
|
permalink: '/blog/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
list:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
category:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
tag:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
|
||||||
|
robots:
|
||||||
|
index: false
|
||||||
|
|
||||||
|
isRelatedPostsEnabled: true # If a widget with related posts is to be displayed below each post
|
||||||
|
relatedPostsCount: 4 # Number of related posts to display
|
||||||
|
|
||||||
|
analytics:
|
||||||
|
vendors:
|
||||||
|
googleAnalytics:
|
||||||
|
id: null # or "G-XXXXXXXXXX"
|
||||||
|
|
||||||
|
ui:
|
||||||
|
theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
#### Customize Design
|
||||||
|
|
||||||
|
To customize Font families, Colors or more Elements refer to the following files:
|
||||||
|
|
||||||
|
- `src/components/CustomStyles.astro`
|
||||||
|
- `src/assets/styles/tailwind.css`
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
#### Deploy to production (manual)
|
||||||
|
|
||||||
|
You can create an optimized production build with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, your website is ready to be deployed. All generated files are located at
|
||||||
|
`dist` folder, which you can deploy the folder to any hosting service you
|
||||||
|
prefer.
|
||||||
|
|
||||||
|
#### Deploy to Netlify
|
||||||
|
|
||||||
|
Clone this repository on your own GitHub account and deploy it to Netlify:
|
||||||
|
|
||||||
|
[![Netlify Deploy button](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/onwidget/astrowind)
|
||||||
|
|
||||||
|
#### Deploy to Vercel
|
||||||
|
|
||||||
|
Clone this repository on your own GitHub account and deploy to Vercel:
|
||||||
|
|
||||||
|
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonwidget%2Fastrowind)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
- Why?
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Related projects
|
||||||
|
|
||||||
|
- [TailNext](https://tailnext.vercel.app/) - Free template using Next.js 14 and Tailwind CSS with the new App Router.
|
||||||
|
- [Qwind](https://qwind.pages.dev/) - Free template to make your website using Qwik + Tailwind CSS.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you have any ideas, suggestions or find any bugs, feel free to open a discussion, an issue or create a pull request.
|
||||||
|
That would be very useful for all of us and we would be happy to listen and take action.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Initially created by [onWidget](https://onwidget.com) and maintained by a community of [contributors](https://github.com/onwidget/astrowind/graphs/contributors).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
**AstroWind** is licensed under the MIT license — see the [LICENSE](./LICENSE.md) file for details.
|
86
astro.config.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
import partytown from '@astrojs/partytown';
|
||||||
|
import icon from 'astro-icon';
|
||||||
|
import compress from 'astro-compress';
|
||||||
|
import type { AstroIntegration } from 'astro';
|
||||||
|
|
||||||
|
import astrowind from './vendor/integration';
|
||||||
|
|
||||||
|
import { readingTimeRemarkPlugin, responsiveTablesRehypePlugin, lazyImagesRehypePlugin } from './src/utils/frontmatter';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const hasExternalScripts = false;
|
||||||
|
const whenExternalScripts = (items: (() => AstroIntegration) | (() => AstroIntegration)[] = []) =>
|
||||||
|
hasExternalScripts ? (Array.isArray(items) ? items.map((item) => item()) : [items()]) : [];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'static',
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
tailwind({
|
||||||
|
applyBaseStyles: false,
|
||||||
|
}),
|
||||||
|
sitemap(),
|
||||||
|
mdx(),
|
||||||
|
icon({
|
||||||
|
include: {
|
||||||
|
tabler: ['*'],
|
||||||
|
'flat-color-icons': [
|
||||||
|
'template',
|
||||||
|
'gallery',
|
||||||
|
'approval',
|
||||||
|
'document',
|
||||||
|
'advertising',
|
||||||
|
'currency-exchange',
|
||||||
|
'voice-presentation',
|
||||||
|
'business-contact',
|
||||||
|
'database',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...whenExternalScripts(() =>
|
||||||
|
partytown({
|
||||||
|
config: { forward: ['dataLayer.push'] },
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
compress({
|
||||||
|
CSS: true,
|
||||||
|
HTML: {
|
||||||
|
'html-minifier-terser': {
|
||||||
|
removeAttributeQuotes: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Image: false,
|
||||||
|
JavaScript: true,
|
||||||
|
SVG: false,
|
||||||
|
Logger: 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
astrowind({
|
||||||
|
config: './src/config.yaml',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [readingTimeRemarkPlugin],
|
||||||
|
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
|
||||||
|
},
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
59
eslint.config.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import astroEslintParser from 'astro-eslint-parser';
|
||||||
|
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||||
|
import globals from 'globals';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import typescriptParser from '@typescript-eslint/parser';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...eslintPluginAstro.configs['flat/recommended'],
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.astro'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: astroEslintParser,
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extraFileExtensions: ['.astro'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx,astro}'],
|
||||||
|
rules: {
|
||||||
|
'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Define the configuration for `<script>` tag.
|
||||||
|
// Script in `<script>` is assigned a virtual file name with the `.js` extension.
|
||||||
|
files: ['**/*.{ts,tsx}', '**/*.astro/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Note: you must disable the base rule as it can report incorrect errors
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['dist', 'node_modules', '.github', 'types.generated.d.ts', '.astro'],
|
||||||
|
},
|
||||||
|
];
|
9
netlify.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[build]
|
||||||
|
publish = "dist"
|
||||||
|
command = "npm run build"
|
||||||
|
[build.processing.html]
|
||||||
|
pretty_urls = false
|
||||||
|
[[headers]]
|
||||||
|
for = "/_astro/*"
|
||||||
|
[headers.values]
|
||||||
|
Cache-Control = "public, max-age=31536000, immutable"
|
10630
package-lock.json
generated
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "bijlobke",
|
||||||
|
"version": "1.0.0-beta.48",
|
||||||
|
"description": "AstroWind: A free template using Astro 4.0 and Tailwind CSS. Astro starter theme.",
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.1 || ^20.3.0 || >= 21.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"check": "npm run check:astro && npm run check:eslint && npm run check:prettier",
|
||||||
|
"check:astro": "astro check",
|
||||||
|
"check:eslint": "eslint .",
|
||||||
|
"check:prettier": "prettier --check .",
|
||||||
|
"fix": "npm run fix:eslint && npm run fix:prettier",
|
||||||
|
"fix:eslint": "eslint --fix .",
|
||||||
|
"fix:prettier": "prettier -w ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/rss": "^4.0.8",
|
||||||
|
"@astrojs/sitemap": "^3.2.0",
|
||||||
|
"@astrolib/analytics": "^0.6.1",
|
||||||
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
|
"@fontsource-variable/inter": "^5.1.0",
|
||||||
|
"astro": "^4.16.2",
|
||||||
|
"astro-embed": "^0.7.4",
|
||||||
|
"astro-icon": "^1.1.1",
|
||||||
|
"limax": "4.1.0",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"unpic": "^3.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/mdx": "^3.1.8",
|
||||||
|
"@astrojs/partytown": "^2.1.2",
|
||||||
|
"@astrojs/tailwind": "5.1.2",
|
||||||
|
"@eslint/js": "^9.12.0",
|
||||||
|
"@iconify-json/flat-color-icons": "^1.2.0",
|
||||||
|
"@iconify-json/tabler": "^1.2.5",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/lodash.merge": "^4.6.9",
|
||||||
|
"@types/mdx": "^2.0.13",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||||
|
"@typescript-eslint/parser": "^8.8.1",
|
||||||
|
"astro-compress": "2.3.3",
|
||||||
|
"astro-eslint-parser": "^1.0.3",
|
||||||
|
"eslint": "^9.12.0",
|
||||||
|
"eslint-plugin-astro": "^1.2.4",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"sharp": "0.33.5",
|
||||||
|
"tailwind-merge": "^2.5.3",
|
||||||
|
"tailwindcss": "^3.4.13",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "^8.8.1",
|
||||||
|
"unist-util-visit": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
2
public/_headers
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/_astro/*
|
||||||
|
Cache-Control: public, max-age=31536000, immutable
|
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
11
sandbox.config.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"infiniteLoopProtection": true,
|
||||||
|
"hardReloadOnChange": false,
|
||||||
|
"view": "browser",
|
||||||
|
"template": "node",
|
||||||
|
"container": {
|
||||||
|
"port": 3000,
|
||||||
|
"startScript": "start",
|
||||||
|
"node": "18"
|
||||||
|
}
|
||||||
|
}
|
BIN
src/assets/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/assets/favicons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
9
src/assets/favicons/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
BIN
src/assets/images/app-store.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/binnen-2.webp
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
src/assets/images/binnen.webp
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
src/assets/images/buiten.webp
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
src/assets/images/buiten3.webp
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
src/assets/images/default.png
Normal file
After Width: | Height: | Size: 563 KiB |
BIN
src/assets/images/google-play.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/grond.webp
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
src/assets/images/hero-image.png
Normal file
After Width: | Height: | Size: 539 KiB |
BIN
src/assets/images/steiger.webp
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
src/assets/images/teras.webp
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
src/assets/images/terras.avif
Normal file
BIN
src/assets/images/terras.webp
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
src/assets/images/vuurplaats.webp
Normal file
After Width: | Height: | Size: 124 KiB |
135
src/assets/styles/tailwind.css
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.bg-page {
|
||||||
|
background-color: var(--aw-color-bg-page);
|
||||||
|
}
|
||||||
|
.bg-dark {
|
||||||
|
background-color: var(--aw-color-bg-page-dark);
|
||||||
|
}
|
||||||
|
.bg-light {
|
||||||
|
background-color: var(--aw-color-bg-page);
|
||||||
|
}
|
||||||
|
.text-page {
|
||||||
|
color: var(--aw-color-text-page);
|
||||||
|
}
|
||||||
|
.text-muted {
|
||||||
|
color: var(--aw-color-text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tertiary {
|
||||||
|
@apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#header.scroll > div:first-child {
|
||||||
|
@apply bg-page md:bg-white/90 md:backdrop-blur-md;
|
||||||
|
box-shadow: 0 0.375rem 1.5rem 0 rgb(140 152 164 / 13%);
|
||||||
|
}
|
||||||
|
.dark #header.scroll > div:first-child,
|
||||||
|
#header.scroll.dark > div:first-child {
|
||||||
|
@apply bg-page md:bg-[#030621e6] border-b border-gray-500/20;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
/* #header.scroll > div:last-child {
|
||||||
|
@apply py-3;
|
||||||
|
} */
|
||||||
|
|
||||||
|
#header.expanded nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 70px !important;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown:focus .dropdown-menu,
|
||||||
|
.dropdown:focus-within .dropdown-menu,
|
||||||
|
.dropdown:hover .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[astro-icon].icon-light > * {
|
||||||
|
stroke-width: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[astro-icon].icon-bold > * {
|
||||||
|
stroke-width: 2.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-aw-toggle-menu] path {
|
||||||
|
@apply transition;
|
||||||
|
}
|
||||||
|
[data-aw-toggle-menu].expanded g > path:first-child {
|
||||||
|
@apply -rotate-45 translate-y-[15px] translate-x-[-3px];
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-aw-toggle-menu].expanded g > path:last-child {
|
||||||
|
@apply rotate-45 translate-y-[-8px] translate-x-[14px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* To deprecated */
|
||||||
|
|
||||||
|
.dd *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.image-container {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.image-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
padding-top: 100px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgb(0,0,0);
|
||||||
|
background-color: rgba(0,0,0,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 35px;
|
||||||
|
color: white;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
63
src/components/CustomStyles.astro
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import '@fontsource-variable/inter';
|
||||||
|
|
||||||
|
// 'DM Sans'
|
||||||
|
// Nunito
|
||||||
|
// Dosis
|
||||||
|
// Outfit
|
||||||
|
// Roboto
|
||||||
|
// Literata
|
||||||
|
// 'IBM Plex Sans'
|
||||||
|
// Karla
|
||||||
|
// Poppins
|
||||||
|
// 'Fira Sans'
|
||||||
|
// 'Libre Franklin'
|
||||||
|
// Inconsolata
|
||||||
|
// Raleway
|
||||||
|
// Oswald
|
||||||
|
// 'Space Grotesk'
|
||||||
|
// Urbanist
|
||||||
|
---
|
||||||
|
|
||||||
|
<style is:inline>
|
||||||
|
:root {
|
||||||
|
--aw-font-sans: 'Inter Variable';
|
||||||
|
--aw-font-serif: 'Inter Variable';
|
||||||
|
--aw-font-heading: 'Inter Variable';
|
||||||
|
|
||||||
|
--aw-color-primary: #73c031;
|
||||||
|
--aw-color-secondary: #61a02b;
|
||||||
|
--aw-color-accent: rgb(109 40 217);
|
||||||
|
|
||||||
|
--aw-color-text-heading: rgb(0 0 0);
|
||||||
|
--aw-color-text-default: rgb(16 16 16);
|
||||||
|
--aw-color-text-muted: rgb(16 16 16 / 66%);
|
||||||
|
--aw-color-bg-page: rgb(255 255 255);
|
||||||
|
|
||||||
|
--aw-color-bg-page-dark: rgb(3 6 32);
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: lavender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--aw-font-sans: 'Inter Variable';
|
||||||
|
--aw-font-serif: 'Inter Variable';
|
||||||
|
--aw-font-heading: 'Inter Variable';
|
||||||
|
|
||||||
|
--aw-color-primary: rgb(1 97 239);
|
||||||
|
--aw-color-secondary: rgb(1 84 207);
|
||||||
|
--aw-color-accent: rgb(109 40 217);
|
||||||
|
|
||||||
|
--aw-color-text-heading: rgb(247, 248, 248);
|
||||||
|
--aw-color-text-default: rgb(229 236 246);
|
||||||
|
--aw-color-text-muted: rgb(229 236 246 / 66%);
|
||||||
|
--aw-color-bg-page: rgb(3 6 32);
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: black;
|
||||||
|
color: snow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
10
src/components/Favicons.astro
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import favIcon from '~/assets/favicons/favicon.ico';
|
||||||
|
import favIconSvg from '~/assets/favicons/favicon.svg';
|
||||||
|
import appleTouchIcon from '~/assets/favicons/apple-touch-icon.png';
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- <link rel="shortcut icon" href={favIcon} /> -->
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href={favIconSvg.src} /> -->
|
||||||
|
<!-- <link rel="mask-icon" href={favIconSvg.src} color="#8D46E7" /> -->
|
||||||
|
<!-- <link rel="apple-touch-icon" sizes="180x180" href={appleTouchIcon.src} /> -->
|
11
src/components/ImageComponent.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
// Props passed to the component
|
||||||
|
const { src, alt } = Astro.props;
|
||||||
|
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="image-container motion-safe:md:animate-fade motion-safe:md:opacity-0">
|
||||||
|
<Image src={src} alt={alt} />
|
||||||
|
</div>
|
9
src/components/Logo.astro
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="self-center ml-2 rtl:ml-0 rtl:mr-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white"
|
||||||
|
>
|
||||||
|
{SITE?.name}
|
||||||
|
</span>
|
14
src/components/blog/Grid.astro
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import Item from '~/components/blog/GridItem.astro';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
posts: Array<Post>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="grid gap-6 row-gap-5 md:grid-cols-2 lg:grid-cols-4 -mb-6">
|
||||||
|
{posts.map((post) => <Item post={post} />)}
|
||||||
|
</div>
|
71
src/components/blog/GridItem.astro
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
|
||||||
|
import { findImage } from '~/utils/images';
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const image = await findImage(post.image);
|
||||||
|
|
||||||
|
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="mb-6 transition intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
>
|
||||||
|
<div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
|
||||||
|
{
|
||||||
|
image &&
|
||||||
|
(link ? (
|
||||||
|
<a href={link}>
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={400}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
layout="cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={400}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
layout="cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
|
||||||
|
{
|
||||||
|
link ? (
|
||||||
|
<a class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200" href={link}>
|
||||||
|
{post.title}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
post.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>
|
||||||
|
</article>
|
12
src/components/blog/Headline.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="mb-8 md:mb-16 text-center max-w-3xl mx-auto">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading" set:html={title} />
|
||||||
|
{
|
||||||
|
subtitle && (
|
||||||
|
<div class="mt-2 md:mt-3 mx-auto text-xl text-gray-500 dark:text-slate-400 font-medium" set:html={subtitle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</header>
|
20
src/components/blog/List.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import Item from '~/components/blog/ListItem.astro';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
posts: Array<Post>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
posts.map((post) => (
|
||||||
|
<li class="mb-12 md:mb-20">
|
||||||
|
<Item post={post} />
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
120
src/components/blog/ListItem.astro
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
import type { ImageMetadata } from 'astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import PostTags from '~/components/blog/Tags.astro';
|
||||||
|
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
import { findImage } from '~/utils/images';
|
||||||
|
import { getFormattedDate } from '~/utils/utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||||
|
|
||||||
|
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<article
|
||||||
|
class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade ${image ? 'md:grid-cols-2' : ''}`}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
image &&
|
||||||
|
(link ? (
|
||||||
|
<a class="relative block group" href={link ?? 'javascript:void(0)'}>
|
||||||
|
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={900}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={900}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<div class="mt-2">
|
||||||
|
<header>
|
||||||
|
<div class="mb-1">
|
||||||
|
<span class="text-sm">
|
||||||
|
<Icon name="tabler:clock" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
|
||||||
|
{
|
||||||
|
post.author && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
· <Icon name="tabler:user" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<span>{post.author.replaceAll('-', ' ')}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.category && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
<a class="hover:underline" href={getPermalink(post.category.slug, 'category')}>
|
||||||
|
{post.category.title}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
|
||||||
|
{
|
||||||
|
link ? (
|
||||||
|
<a
|
||||||
|
class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200"
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
|
{post.title}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
post.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{post.excerpt && <p class="flex-grow text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>}
|
||||||
|
{
|
||||||
|
post.tags && Array.isArray(post.tags) ? (
|
||||||
|
<footer class="mt-5">
|
||||||
|
<PostTags tags={post.tags} />
|
||||||
|
</footer>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</article>
|
36
src/components/blog/Pagination.astro
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
prevUrl?: string;
|
||||||
|
nextUrl?: string;
|
||||||
|
prevText?: string;
|
||||||
|
nextText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
(prevUrl || nextUrl) && (
|
||||||
|
<div class="container flex">
|
||||||
|
<div class="flex flex-row mx-auto container justify-between">
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
class={`md:px-3 px-3 mr-2 ${!prevUrl ? 'invisible' : ''}`}
|
||||||
|
href={getPermalink(prevUrl)}
|
||||||
|
>
|
||||||
|
<Icon name="tabler:chevron-left" class="w-6 h-6" />
|
||||||
|
<p class="ml-2">{prevText}</p>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="tertiary" class={`md:px-3 px-3 ${!nextUrl ? 'invisible' : ''}`} href={getPermalink(nextUrl)}>
|
||||||
|
<span class="mr-2">{nextText}</span>
|
||||||
|
<Icon name="tabler:chevron-right" class="w-6 h-6" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
31
src/components/blog/RelatedPosts.astro
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import { getRelatedPosts } from '~/utils/blog';
|
||||||
|
import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
|
||||||
|
const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isRelatedPostsEnabled ? (
|
||||||
|
<BlogHighlightedPosts
|
||||||
|
classes={{
|
||||||
|
container:
|
||||||
|
'pt-0 lg:pt-0 md:pt-0 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||||
|
}}
|
||||||
|
title="Related Posts"
|
||||||
|
linkText="View All Posts"
|
||||||
|
linkUrl={getBlogPermalink()}
|
||||||
|
postIds={relatedPosts.map((post) => post.id)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
103
src/components/blog/SinglePost.astro
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import PostTags from '~/components/blog/Tags.astro';
|
||||||
|
import SocialShare from '~/components/common/SocialShare.astro';
|
||||||
|
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
import { getFormattedDate } from '~/utils/utils';
|
||||||
|
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
url: string | URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post, url } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="py-8 sm:py-16 lg:py-20 mx-auto">
|
||||||
|
<article>
|
||||||
|
<header
|
||||||
|
class={post.image
|
||||||
|
? 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'
|
||||||
|
: 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'}
|
||||||
|
>
|
||||||
|
<div class="flex justify-between flex-col sm:flex-row max-w-3xl mx-auto mt-0 mb-2 px-4 sm:px-6 sm:items-center">
|
||||||
|
<p>
|
||||||
|
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
|
||||||
|
{
|
||||||
|
post.author && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
· <Icon name="tabler:user" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<span class="inline-block">{post.author}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.category && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
<a class="hover:underline inline-block" href={getPermalink(post.category.slug, 'category')}>
|
||||||
|
{post.category.title}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.readingTime && (
|
||||||
|
<>
|
||||||
|
· <span>{post.readingTime}</span> min read
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
class="px-4 sm:px-6 max-w-3xl mx-auto text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading"
|
||||||
|
>
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="max-w-3xl mx-auto mt-4 mb-8 px-4 sm:px-6 text-xl md:text-2xl text-muted dark:text-slate-400 text-justify"
|
||||||
|
>
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{
|
||||||
|
post.image ? (
|
||||||
|
<Image
|
||||||
|
src={post.image}
|
||||||
|
class="max-w-full lg:max-w-[900px] mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post?.excerpt || ''}
|
||||||
|
width={900}
|
||||||
|
height={506}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="border-t dark:border-slate-700" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-md lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8 prose-headings:scroll-mt-[80px] prose-li:my-0"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto px-6 sm:px-6 max-w-3xl mt-8 flex justify-between flex-col sm:flex-row">
|
||||||
|
<PostTags tags={post.tags} class="mr-5 rtl:mr-0 rtl:ml-5" />
|
||||||
|
<SocialShare url={url} text={post.title} class="mt-5 sm:mt-1 align-middle text-gray-500 dark:text-slate-600" />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
45
src/components/blog/Tags.astro
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
tags: Post['tags'];
|
||||||
|
class?: string;
|
||||||
|
title?: string | undefined;
|
||||||
|
isCategory?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
tags && Array.isArray(tags) && (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
{title !== undefined && (
|
||||||
|
<span class="align-super font-normal underline underline-offset-4 decoration-2 dark:text-slate-400">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
<ul class={className}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 rtl:mr-0 rtl:ml-2 mb-2 py-0.5 px-2 lowercase font-medium">
|
||||||
|
{!APP_BLOG?.tag?.isEnabled ? (
|
||||||
|
tag.title
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={getPermalink(tag.slug, isCategory ? 'category' : 'tag')}
|
||||||
|
class="text-muted dark:text-slate-300 hover:text-primary dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
{tag.title}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
20
src/components/blog/ToBlogLink.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
import { I18N } from 'astrowind:config';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
const { textDirection } = I18N;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mx-auto px-6 sm:px-6 max-w-3xl pt-8 md:pt-4 pb-12 md:pb-20">
|
||||||
|
<Button variant="tertiary" class="px-3 md:px-3" href={getBlogPermalink()}>
|
||||||
|
{
|
||||||
|
textDirection === 'rtl' ? (
|
||||||
|
<Icon name="tabler:chevron-right" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
|
||||||
|
) : (
|
||||||
|
<Icon name="tabler:chevron-left" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
|
||||||
|
)
|
||||||
|
} Back to Blog
|
||||||
|
</Button>
|
||||||
|
</div>
|
13
src/components/common/Analytics.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { GoogleAnalytics } from '@astrolib/analytics';
|
||||||
|
import { ANALYTICS } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
ANALYTICS?.vendors?.googleAnalytics?.id ? (
|
||||||
|
<GoogleAnalytics
|
||||||
|
id={String(ANALYTICS.vendors.googleAnalytics.id)}
|
||||||
|
partytown={ANALYTICS?.vendors?.googleAnalytics?.partytown}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
33
src/components/common/ApplyColorMode.astro
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import { UI } from 'astrowind:config';
|
||||||
|
|
||||||
|
// TODO: This code is temporary
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ defaultTheme: UI.theme || 'system' }}>
|
||||||
|
function applyTheme(theme) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
const matches = document.querySelectorAll('[data-aw-toggle-color-scheme] > input');
|
||||||
|
|
||||||
|
if (matches && matches.length) {
|
||||||
|
matches.forEach((elem) => {
|
||||||
|
elem.checked = theme !== 'dark';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
|
||||||
|
applyTheme(defaultTheme.replace(':only', ''));
|
||||||
|
} else if (
|
||||||
|
localStorage.theme === 'dark' ||
|
||||||
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
|
applyTheme('dark');
|
||||||
|
} else {
|
||||||
|
applyTheme('light');
|
||||||
|
}
|
||||||
|
</script>
|
255
src/components/common/BasicScripts.astro
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
---
|
||||||
|
import { UI } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ defaultTheme: UI.theme }}>
|
||||||
|
if (window.basic_script) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.basic_script = true;
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initTheme = function () {
|
||||||
|
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
|
||||||
|
applyTheme(defaultTheme.replace(':only', ''));
|
||||||
|
} else if (
|
||||||
|
localStorage.theme === 'dark' ||
|
||||||
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
|
applyTheme('dark');
|
||||||
|
} else {
|
||||||
|
applyTheme('light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initTheme();
|
||||||
|
|
||||||
|
function attachEvent(selector, event, fn) {
|
||||||
|
const matches = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
|
||||||
|
if (matches && matches.length) {
|
||||||
|
matches.forEach((elem) => {
|
||||||
|
elem.addEventListener(event, (e) => fn(e, elem), false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoad = function () {
|
||||||
|
let lastKnownScrollPosition = window.scrollY;
|
||||||
|
let ticking = true;
|
||||||
|
|
||||||
|
attachEvent('#header nav', 'click', function () {
|
||||||
|
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
|
||||||
|
document.body.classList.remove('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.remove('h-screen');
|
||||||
|
document.getElementById('header')?.classList.remove('expanded');
|
||||||
|
document.getElementById('header')?.classList.remove('bg-page');
|
||||||
|
document.querySelector('#header nav')?.classList.add('hidden');
|
||||||
|
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachEvent('[data-aw-toggle-menu]', 'click', function (_, elem) {
|
||||||
|
elem.classList.toggle('expanded');
|
||||||
|
document.body.classList.toggle('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.toggle('h-screen');
|
||||||
|
document.getElementById('header')?.classList.toggle('expanded');
|
||||||
|
document.getElementById('header')?.classList.toggle('bg-page');
|
||||||
|
document.querySelector('#header nav')?.classList.toggle('hidden');
|
||||||
|
document.querySelector('#header > div > div:last-child')?.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachEvent('[data-aw-toggle-color-scheme]', 'click', function () {
|
||||||
|
if (defaultTheme.endsWith(':only')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
attachEvent('[data-aw-social-share]', 'click', function (_, elem) {
|
||||||
|
const network = elem.getAttribute('data-aw-social-share');
|
||||||
|
const url = encodeURIComponent(elem.getAttribute('data-aw-url'));
|
||||||
|
const text = encodeURIComponent(elem.getAttribute('data-aw-text'));
|
||||||
|
|
||||||
|
let href;
|
||||||
|
switch (network) {
|
||||||
|
case 'facebook':
|
||||||
|
href = `https://www.facebook.com/sharer.php?u=${url}`;
|
||||||
|
break;
|
||||||
|
case 'twitter':
|
||||||
|
href = `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
|
||||||
|
break;
|
||||||
|
case 'linkedin':
|
||||||
|
href = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`;
|
||||||
|
break;
|
||||||
|
case 'whatsapp':
|
||||||
|
href = `https://wa.me/?text=${text}%20${url}`;
|
||||||
|
break;
|
||||||
|
case 'mail':
|
||||||
|
href = `mailto:?subject=%22${text}%22&body=${text}%20${url}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newlink = document.createElement('a');
|
||||||
|
newlink.target = '_blank';
|
||||||
|
newlink.href = href;
|
||||||
|
newlink.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
const screenSize = window.matchMedia('(max-width: 767px)');
|
||||||
|
screenSize.addEventListener('change', function () {
|
||||||
|
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
|
||||||
|
document.body.classList.remove('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.remove('h-screen');
|
||||||
|
document.getElementById('header')?.classList.remove('expanded');
|
||||||
|
document.getElementById('header')?.classList.remove('bg-page');
|
||||||
|
document.querySelector('#header nav')?.classList.add('hidden');
|
||||||
|
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyHeaderStylesOnScroll() {
|
||||||
|
const header = document.querySelector('#header[data-aw-sticky-header]');
|
||||||
|
if (!header) return;
|
||||||
|
if (lastKnownScrollPosition > 60 && !header.classList.contains('scroll')) {
|
||||||
|
header.classList.add('scroll');
|
||||||
|
} else if (lastKnownScrollPosition <= 60 && header.classList.contains('scroll')) {
|
||||||
|
header.classList.remove('scroll');
|
||||||
|
}
|
||||||
|
ticking = false;
|
||||||
|
}
|
||||||
|
applyHeaderStylesOnScroll();
|
||||||
|
|
||||||
|
attachEvent([document], 'scroll', function () {
|
||||||
|
lastKnownScrollPosition = window.scrollY;
|
||||||
|
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
applyHeaderStylesOnScroll();
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onPageShow = function () {
|
||||||
|
document.documentElement.classList.add('motion-safe:scroll-smooth');
|
||||||
|
const elem = document.querySelector('[data-aw-toggle-menu]');
|
||||||
|
if (elem) {
|
||||||
|
elem.classList.remove('expanded');
|
||||||
|
}
|
||||||
|
document.body.classList.remove('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.remove('h-screen');
|
||||||
|
document.getElementById('header')?.classList.remove('expanded');
|
||||||
|
document.querySelector('#header nav')?.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onload = onLoad;
|
||||||
|
window.onpageshow = onPageShow;
|
||||||
|
|
||||||
|
document.addEventListener('astro:after-swap', () => {
|
||||||
|
initTheme();
|
||||||
|
onLoad();
|
||||||
|
onPageShow();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
/* Inspired by: https://github.com/heidkaemper/tailwindcss-intersect */
|
||||||
|
const Observer = {
|
||||||
|
observer: null,
|
||||||
|
delayBetweenAnimations: 100,
|
||||||
|
animationCounter: 0,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const selectors = [
|
||||||
|
'[class*=" intersect:"]',
|
||||||
|
'[class*=":intersect:"]',
|
||||||
|
'[class^="intersect:"]',
|
||||||
|
'[class="intersect"]',
|
||||||
|
'[class*=" intersect "]',
|
||||||
|
'[class^="intersect "]',
|
||||||
|
'[class$=" intersect"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
const elements = Array.from(document.querySelectorAll(selectors.join(',')));
|
||||||
|
|
||||||
|
const getThreshold = (element) => {
|
||||||
|
if (element.classList.contains('intersect-full')) return 0.99;
|
||||||
|
if (element.classList.contains('intersect-half')) return 0.5;
|
||||||
|
if (element.classList.contains('intersect-quarter')) return 0.25;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
el.setAttribute('no-intersect', '');
|
||||||
|
el._intersectionThreshold = getThreshold(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
const callback = (entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const target = entry.target;
|
||||||
|
const intersectionRatio = entry.intersectionRatio;
|
||||||
|
const threshold = target._intersectionThreshold;
|
||||||
|
|
||||||
|
if (target.classList.contains('intersect-no-queue')) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
target.removeAttribute('no-intersect');
|
||||||
|
if (target.classList.contains('intersect-once')) {
|
||||||
|
this.observer.unobserve(target);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.setAttribute('no-intersect', '');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionRatio >= threshold) {
|
||||||
|
if (!target.hasAttribute('data-animated')) {
|
||||||
|
target.removeAttribute('no-intersect');
|
||||||
|
target.setAttribute('data-animated', 'true');
|
||||||
|
|
||||||
|
const delay = this.animationCounter * this.delayBetweenAnimations;
|
||||||
|
this.animationCounter++;
|
||||||
|
|
||||||
|
target.style.transitionDelay = `${delay}ms`;
|
||||||
|
target.style.animationDelay = `${delay}ms`;
|
||||||
|
|
||||||
|
if (target.classList.contains('intersect-once')) {
|
||||||
|
this.observer.unobserve(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.setAttribute('no-intersect', '');
|
||||||
|
target.removeAttribute('data-animated');
|
||||||
|
target.style.transitionDelay = '';
|
||||||
|
target.style.animationDelay = '';
|
||||||
|
|
||||||
|
this.animationCounter = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver(callback.bind(this), { threshold: [0, 0.25, 0.5, 0.99] });
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
this.observer.observe(el);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Observer.start();
|
||||||
|
|
||||||
|
document.addEventListener('astro:after-swap', () => {
|
||||||
|
Observer.start();
|
||||||
|
});
|
||||||
|
</script>
|
8
src/components/common/CommonMeta.astro
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import { getAsset } from '~/utils/permalinks';
|
||||||
|
---
|
||||||
|
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />
|
61
src/components/common/Image.astro
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLAttributes } from 'astro/types';
|
||||||
|
import { findImage } from '~/utils/images';
|
||||||
|
import {
|
||||||
|
getImagesOptimized,
|
||||||
|
astroAsseetsOptimizer,
|
||||||
|
unpicOptimizer,
|
||||||
|
isUnpicCompatible,
|
||||||
|
type ImageProps,
|
||||||
|
} from '~/utils/images-optimization';
|
||||||
|
|
||||||
|
type Props = ImageProps;
|
||||||
|
type ImageType = {
|
||||||
|
src: string;
|
||||||
|
attributes: HTMLAttributes<'img'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = Astro.props;
|
||||||
|
|
||||||
|
if (props.alt === undefined || props.alt === null) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.width === 'string') {
|
||||||
|
props.width = parseInt(props.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.height === 'string') {
|
||||||
|
props.height = parseInt(props.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.loading) {
|
||||||
|
props.loading = 'lazy';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.decoding) {
|
||||||
|
props.decoding = 'async';
|
||||||
|
}
|
||||||
|
|
||||||
|
const _image = await findImage(props.src);
|
||||||
|
|
||||||
|
let image: ImageType | undefined = undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof _image === 'string' &&
|
||||||
|
(_image.startsWith('http://') || _image.startsWith('https://')) &&
|
||||||
|
isUnpicCompatible(_image)
|
||||||
|
) {
|
||||||
|
image = await getImagesOptimized(_image, props, unpicOptimizer);
|
||||||
|
} else if (_image) {
|
||||||
|
image = await getImagesOptimized(_image, props, astroAsseetsOptimizer);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
!image ? (
|
||||||
|
<Fragment />
|
||||||
|
) : (
|
||||||
|
<img src={image.src} crossorigin="anonymous" referrerpolicy="no-referrer" {...image.attributes} />
|
||||||
|
)
|
||||||
|
}
|
68
src/components/common/Metadata.astro
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
import merge from 'lodash.merge';
|
||||||
|
import { AstroSeo } from '@astrolib/seo';
|
||||||
|
|
||||||
|
import type { Props as AstroSeoProps } from '@astrolib/seo';
|
||||||
|
|
||||||
|
import { SITE, METADATA, I18N } from 'astrowind:config';
|
||||||
|
import type { MetaData } from '~/types';
|
||||||
|
import { getCanonical } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
import { adaptOpenGraphImages } from '~/utils/images';
|
||||||
|
|
||||||
|
export interface Props extends MetaData {
|
||||||
|
dontUseTitleTemplate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
ignoreTitleTemplate = false,
|
||||||
|
canonical = String(getCanonical(String(Astro.url.pathname))),
|
||||||
|
robots = {},
|
||||||
|
description,
|
||||||
|
openGraph = {},
|
||||||
|
twitter = {},
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const seoProps: AstroSeoProps = merge(
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
titleTemplate: '%s',
|
||||||
|
canonical: canonical,
|
||||||
|
noindex: true,
|
||||||
|
nofollow: true,
|
||||||
|
description: undefined,
|
||||||
|
openGraph: {
|
||||||
|
url: canonical,
|
||||||
|
site_name: SITE?.name,
|
||||||
|
images: [],
|
||||||
|
locale: I18N?.language || 'en',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
cardType: openGraph?.images?.length ? 'summary_large_image' : 'summary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: METADATA?.title?.default,
|
||||||
|
titleTemplate: METADATA?.title?.template,
|
||||||
|
noindex: typeof METADATA?.robots?.index !== 'undefined' ? !METADATA.robots.index : undefined,
|
||||||
|
nofollow: typeof METADATA?.robots?.follow !== 'undefined' ? !METADATA.robots.follow : undefined,
|
||||||
|
description: METADATA?.description,
|
||||||
|
openGraph: METADATA?.openGraph,
|
||||||
|
twitter: METADATA?.twitter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: title,
|
||||||
|
titleTemplate: ignoreTitleTemplate ? '%s' : undefined,
|
||||||
|
canonical: canonical,
|
||||||
|
noindex: typeof robots?.index !== 'undefined' ? !robots.index : undefined,
|
||||||
|
nofollow: typeof robots?.follow !== 'undefined' ? !robots.follow : undefined,
|
||||||
|
description: description,
|
||||||
|
openGraph: { url: canonical, ...openGraph },
|
||||||
|
twitter: twitter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<AstroSeo {...{ ...seoProps, openGraph: await adaptOpenGraphImages(seoProps?.openGraph, Astro.site) }} />
|
5
src/components/common/SiteVerification.astro
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}
|
65
src/components/common/SocialShare.astro
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
text: string;
|
||||||
|
url: string | URL;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text, url, class: className = 'inline-block' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={className}>
|
||||||
|
<span class="align-super font-bold text-slate-500 dark:text-slate-400">Share:</span>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Twitter Share"
|
||||||
|
data-aw-social-share="twitter"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-x"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button class="ml-2 rtl:ml-0 rtl:mr-2" title="Facebook Share" data-aw-social-share="facebook" data-aw-url={url}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-facebook"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Linkedin Share"
|
||||||
|
data-aw-social-share="linkedin"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-linkedin"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Whatsapp Share"
|
||||||
|
data-aw-social-share="whatsapp"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-whatsapp"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Email Share"
|
||||||
|
data-aw-social-share="mail"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:mail"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
6
src/components/common/SplitbeeAnalytics.astro
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
const { doNotTrack = true, noCookieMode = false, url = 'https://cdn.splitbee.io/sb.js' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Splitbee Analytics -->
|
||||||
|
<script is:inline data-respect-dnt={doNotTrack} data-no-cookie={noCookieMode} async src={url}></script>
|
29
src/components/common/ToggleMenu.astro
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
label?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
label = 'Toggle Menu',
|
||||||
|
class: className = 'flex flex-col h-12 w-12 rounded justify-center items-center cursor-pointer group',
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<button type="button" class={className} aria-label={label} data-aw-toggle-menu>
|
||||||
|
<span class="sr-only">{label}</span>
|
||||||
|
<slot>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:rotate-45 group-[.expanded]:translate-y-2.5"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:opacity-0"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:-rotate-45 group-[.expanded]:-translate-y-2.5"
|
||||||
|
></span>
|
||||||
|
</slot>
|
||||||
|
</button>
|
28
src/components/common/ToggleTheme.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import { UI } from 'astrowind:config';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
label?: string;
|
||||||
|
class?: string;
|
||||||
|
iconClass?: string;
|
||||||
|
iconName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
label = 'Toggle between Dark and Light mode',
|
||||||
|
class:
|
||||||
|
className = 'text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center',
|
||||||
|
iconClass = 'w-6 h-6',
|
||||||
|
iconName = 'tabler:sun',
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
!(UI.theme && UI.theme.endsWith(':only')) && (
|
||||||
|
<button type="button" class={className} aria-label={label} data-aw-toggle-color-scheme>
|
||||||
|
<Icon name={iconName} class={iconClass} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
11
src/components/ui/Background.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
isDark?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isDark = false } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
40
src/components/ui/Button.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { CallToAction as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant = 'secondary',
|
||||||
|
target,
|
||||||
|
text = Astro.slots.render('default'),
|
||||||
|
icon = '',
|
||||||
|
class: className = '',
|
||||||
|
type,
|
||||||
|
...rest
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
primary: 'btn-primary',
|
||||||
|
secondary: 'btn-secondary',
|
||||||
|
tertiary: 'btn btn-tertiary',
|
||||||
|
link: 'cursor-pointer hover:text-primary',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
type === 'button' || type === 'submit' || type === 'reset' ? (
|
||||||
|
<button type={type} class={twMerge(variants[variant] || '', className)} {...rest}>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class={twMerge(variants[variant] || '', className)}
|
||||||
|
{...(target ? { target: target, rel: 'noopener noreferrer' } : {})}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
22
src/components/ui/DListItem.astro
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
// component: DListItem
|
||||||
|
//
|
||||||
|
// Mimics the html 'dl' (description list)
|
||||||
|
//
|
||||||
|
// The 'dt' item is the item 'term' and is inserted into an 'h6' tag.
|
||||||
|
// Caller needs to style the 'h6' tag appropriately.
|
||||||
|
//
|
||||||
|
// You can put pretty much any content you want between the open and
|
||||||
|
// closing tags - it's simply contained in an enclosing div with a
|
||||||
|
// margin left. No need for 'dd' items.
|
||||||
|
//
|
||||||
|
const { dt } = Astro.props;
|
||||||
|
interface Props {
|
||||||
|
dt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: string = await Astro.slots.render('default');
|
||||||
|
---
|
||||||
|
|
||||||
|
<h6 set:html={dt} />
|
||||||
|
<div class="dd ml-8" set:html={content} />
|
87
src/components/ui/Form.astro
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import type { Form as Props } from '~/types';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<form>
|
||||||
|
{
|
||||||
|
inputs &&
|
||||||
|
inputs.map(
|
||||||
|
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) =>
|
||||||
|
name && (
|
||||||
|
<div class="mb-6">
|
||||||
|
{label && (
|
||||||
|
<label for={name} class="block text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
id={name}
|
||||||
|
autocomplete={autocomplete}
|
||||||
|
placeholder={placeholder}
|
||||||
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
textarea && (
|
||||||
|
<div>
|
||||||
|
<label for="textarea" class="block text-sm font-medium">
|
||||||
|
{textarea.label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="textarea"
|
||||||
|
name={textarea.name ? textarea.name : 'message'}
|
||||||
|
rows={textarea.rows ? textarea.rows : 4}
|
||||||
|
placeholder={textarea.placeholder}
|
||||||
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
disclaimer && (
|
||||||
|
<div class="mt-3 flex items-start">
|
||||||
|
<div class="flex mt-0.5">
|
||||||
|
<input
|
||||||
|
id="disclaimer"
|
||||||
|
name="disclaimer"
|
||||||
|
type="checkbox"
|
||||||
|
class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{disclaimer.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
button && (
|
||||||
|
<div class="mt-10 grid">
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
{button}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
description && (
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</form>
|
35
src/components/ui/Headline.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import type { Headline as Props } from '~/types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
classes = {},
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = 'max-w-3xl',
|
||||||
|
title: titleClass = 'text-3xl md:text-4xl ',
|
||||||
|
subtitle: subtitleClass = 'text-xl',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
(title || subtitle || tagline) && (
|
||||||
|
<div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
|
||||||
|
{tagline && (
|
||||||
|
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<h2
|
||||||
|
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
65
src/components/ui/ItemGrid.astro
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import type { ItemGrid as Props } from '~/types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from './Button.astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary',
|
||||||
|
action: actionClass = '',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`grid mx-auto gap-8 md:gap-y-12 ${
|
||||||
|
columns === 4
|
||||||
|
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 3
|
||||||
|
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 2
|
||||||
|
? 'sm:grid-cols-2 '
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
containerClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||||
|
<div class="intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
|
||||||
|
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-7 h-7 mr-2 rtl:mr-0 rtl:ml-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5">
|
||||||
|
{title && <h3 class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</h3>}
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
class={twMerge(`${title ? 'mt-3' : ''} text-muted`, descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{callToAction && (
|
||||||
|
<div class={twMerge(`${title || description ? 'mt-3' : ''}`, actionClass, itemClasses?.actionClass)}>
|
||||||
|
<Button variant="link" {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
59
src/components/ui/ItemGrid2.astro
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
import type { ItemGrid as Props } from '~/types';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from './Button.astro';
|
||||||
|
|
||||||
|
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
// container: containerClass = "sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`grid gap-8 gap-x-12 sm:gap-y-8 ${
|
||||||
|
columns === 4
|
||||||
|
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 3
|
||||||
|
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 2
|
||||||
|
? 'sm:grid-cols-2 '
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
containerClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
'relative flex flex-col intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||||
|
panelClass,
|
||||||
|
itemClasses?.panel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
|
||||||
|
)}
|
||||||
|
<div class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</div>
|
||||||
|
{description && (
|
||||||
|
<p class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)} set:html={description} />
|
||||||
|
)}
|
||||||
|
{callToAction && (
|
||||||
|
<div class="mt-2">
|
||||||
|
<Button {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
60
src/components/ui/Timeline.astro
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { Item } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items?: Array<Item>;
|
||||||
|
defaultIcon?: string;
|
||||||
|
classes?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && items.length && (
|
||||||
|
<div class={containerClass}>
|
||||||
|
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||||
|
panelClass,
|
||||||
|
itemClasses?.panel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-10 h-10 p-2 rounded-full border-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
|
||||||
|
</div>
|
||||||
|
<div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
|
||||||
|
{title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
34
src/components/ui/WidgetWrapper.astro
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLTag } from 'astro/types';
|
||||||
|
import type { Widget } from '~/types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Background from './Background.astro';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
containerClass?: string;
|
||||||
|
['as']?: HTMLTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
|
||||||
|
|
||||||
|
const WrapperTag = as;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
twMerge(
|
||||||
|
'relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
|
||||||
|
containerClass
|
||||||
|
),
|
||||||
|
{ dark: isDark },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</WrapperTag>
|
23
src/components/widgets/Announcement.astro
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark text-muted text-sm bg-black dark:bg-transparent dark:border-b dark:border-slate-800 dark:text-slate-400 hidden md:flex gap-1 overflow-hidden px-3 py-2 relative text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="dark:bg-slate-700 bg-white/40 dark:text-slate-300 font-semibold px-1 py-0.5 text-xs mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-block"
|
||||||
|
>NEW</span
|
||||||
|
>
|
||||||
|
<a href="https://astro.build/blog/astro-4150/" class="text-muted hover:underline dark:text-slate-400 font-medium"
|
||||||
|
>Astro 4.15 is now available! »</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="ltr:ml-auto rtl:mr-auto w-[5.6rem] h-[1.25rem] ml-auto bg-contain inline-block bg-[url(https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=Stars&maxAge=86400)]"
|
||||||
|
title="If you like AstroWind, give us a star."
|
||||||
|
href="https://github.com/onwidget/astrowind"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
64
src/components/widgets/BlogHighlightedPosts.astro
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import Grid from '~/components/blog/Grid.astro';
|
||||||
|
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
import { findPostsByIds } from '~/utils/blog';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Widget } from '~/types';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
linkText?: string;
|
||||||
|
linkUrl?: string | URL;
|
||||||
|
information?: string;
|
||||||
|
postIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
linkText = 'View all posts',
|
||||||
|
linkUrl = getBlogPermalink(),
|
||||||
|
information = await Astro.slots.render('information'),
|
||||||
|
postIds = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const posts = APP_BLOG.isEnabled ? await findPostsByIds(postIds) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isEnabled ? (
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}>
|
||||||
|
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
|
||||||
|
{title && (
|
||||||
|
<div class="md:max-w-sm">
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
{APP_BLOG.list.isEnabled && linkText && linkUrl && (
|
||||||
|
<a
|
||||||
|
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0"
|
||||||
|
href={linkUrl}
|
||||||
|
>
|
||||||
|
{linkText} »
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid posts={posts} />
|
||||||
|
</WidgetWrapper>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
63
src/components/widgets/BlogLatestPosts.astro
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import Grid from '~/components/blog/Grid.astro';
|
||||||
|
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
import { findLatestPosts } from '~/utils/blog';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Widget } from '~/types';
|
||||||
|
import Button from '../ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
linkText?: string;
|
||||||
|
linkUrl?: string | URL;
|
||||||
|
information?: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
linkText = 'View all posts',
|
||||||
|
linkUrl = getBlogPermalink(),
|
||||||
|
information = await Astro.slots.render('information'),
|
||||||
|
count = 4,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const posts = APP_BLOG.isEnabled ? await findLatestPosts({ count }) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isEnabled ? (
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}>
|
||||||
|
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
|
||||||
|
{title && (
|
||||||
|
<div class="md:max-w-sm">
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
{APP_BLOG.list.isEnabled && linkText && linkUrl && (
|
||||||
|
<Button variant="link" href={linkUrl}>
|
||||||
|
{' '}
|
||||||
|
{linkText} »
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid posts={posts} />
|
||||||
|
</WidgetWrapper>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
38
src/components/widgets/Brands.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import type { Brands as Props } from '~/types';
|
||||||
|
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
icons = [],
|
||||||
|
images = [],
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-center gap-x-6 sm:gap-x-12 lg:gap-x-24">
|
||||||
|
{icons && icons.map((icon) => <Icon name={icon} class="py-3 lg:py-5 w-12 h-auto mx-auto sm:mx-0 text-gray-500" />)}
|
||||||
|
{
|
||||||
|
images &&
|
||||||
|
images.map(
|
||||||
|
(image) =>
|
||||||
|
image.src && (
|
||||||
|
<div class="flex justify-center col-span-1 my-2 lg:my-4 py-1 px-3 rounded-md dark:bg-gray-200">
|
||||||
|
<Image src={image.src} alt={image.alt || ''} class="max-h-12" width={120} height={48} layout="fixed" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
58
src/components/widgets/CallToAction.astro
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import type { CallToAction, Widget } from '~/types';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
callToAction?: CallToAction;
|
||||||
|
actions?: string | CallToAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div
|
||||||
|
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600"
|
||||||
|
>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'mb-0 md:mb-0',
|
||||||
|
title: 'text-4xl md:text-4xl font-bold tracking-tighter mb-4 font-heading',
|
||||||
|
subtitle: 'text-xl text-muted dark:text-slate-400',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 mt-6">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
40
src/components/widgets/Contact.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import FormContainer from '~/components/ui/Form.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Contact as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
inputs,
|
||||||
|
textarea,
|
||||||
|
disclaimer,
|
||||||
|
button,
|
||||||
|
description,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
{
|
||||||
|
inputs && (
|
||||||
|
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow p-4 sm:p-6 lg:p-8 w-full">
|
||||||
|
<FormContainer
|
||||||
|
inputs={inputs}
|
||||||
|
textarea={textarea}
|
||||||
|
disclaimer={disclaimer}
|
||||||
|
button={button}
|
||||||
|
description={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</WidgetWrapper>
|
94
src/components/widgets/Content.astro
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
import type { Content as Props } from '~/types';
|
||||||
|
import Headline from '../ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import ItemGrid from '../ui/ItemGrid.astro';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
callToAction,
|
||||||
|
items = [],
|
||||||
|
columns,
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
isReversed = false,
|
||||||
|
isAfterContent = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper
|
||||||
|
id={id}
|
||||||
|
isDark={isDark}
|
||||||
|
containerClass={`max-w-7xl mx-auto ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${classes?.container ?? ''}`}
|
||||||
|
bg={bg}
|
||||||
|
>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'max-w-xl sm:mx-auto lg:max-w-2xl',
|
||||||
|
title: 'text-4xl md:text-5xl font-bold tracking-tighter mb-4 font-heading',
|
||||||
|
subtitle: 'max-w-3xl mx-auto sm:text-center text-xl text-muted dark:text-slate-400',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="mx-auto max-w-7xl p-4 md:px-8">
|
||||||
|
<div class={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}>
|
||||||
|
<div class="md:basis-1/2 self-center">
|
||||||
|
{content && <div class="mb-12 text-lg dark:text-slate-400" set:html={content} />}
|
||||||
|
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="mt-[-40px] mb-8 text-primary">
|
||||||
|
<Button variant="link" {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon="tabler:check"
|
||||||
|
classes={{
|
||||||
|
container: `gap-y-4 md:gap-y-8`,
|
||||||
|
panel: 'max-w-none',
|
||||||
|
title: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
description: 'text-muted dark:text-slate-400 ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
icon: 'flex h-7 w-7 items-center justify-center rounded-full bg-green-600 dark:bg-green-700 text-gray-50 p-1',
|
||||||
|
action: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" class="mt-10 md:mt-0 md:basis-1/2">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-4xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto w-full rounded-lg bg-gray-500 shadow-lg"
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
widths={[400, 768]}
|
||||||
|
sizes="(max-width: 768px) 100vw, 432px"
|
||||||
|
layout="responsive"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
33
src/components/widgets/FAQs.astro
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import ItemGrid from '~/components/ui/ItemGrid.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Faqs as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
items = [],
|
||||||
|
columns = 2,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon="tabler:chevron-right"
|
||||||
|
classes={{
|
||||||
|
container: `${columns === 1 ? 'max-w-4xl' : ''} gap-y-8 md:gap-y-12`,
|
||||||
|
panel: 'max-w-none',
|
||||||
|
icon: 'flex-shrink-0 mt-1 w-6 h-6 text-primary',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
36
src/components/widgets/Features.astro
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import ItemGrid from '~/components/ui/ItemGrid.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import type { Features as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
columns = 2,
|
||||||
|
|
||||||
|
defaultIcon,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl py-0 ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: '',
|
||||||
|
title: 'md:text-[1.3rem]',
|
||||||
|
icon: 'text-white bg-primary rounded-full w-10 h-10 p-2 md:w-12 md:h-12 md:p-3 mr-4 rtl:ml-4 rtl:mr-0',
|
||||||
|
...((classes?.items as Record<string, never>) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
38
src/components/widgets/Features2.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import ItemGrid2 from '~/components/ui/ItemGrid2.astro';
|
||||||
|
import type { Features as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
columns = 3,
|
||||||
|
defaultIcon,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
<ItemGrid2
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: 'gap-4 md:gap-6',
|
||||||
|
panel:
|
||||||
|
'rounded-lg shadow-[0_4px_30px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_30px_rgba(0,0,0,0.1)] backdrop-blur border border-[#ffffff29] bg-white dark:bg-slate-900 p-6',
|
||||||
|
// panel:
|
||||||
|
// "text-center bg-page items-center md:text-left rtl:md:text-right md:items-start p-6 p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-800",
|
||||||
|
icon: 'w-12 h-12 mb-6 text-primary',
|
||||||
|
...((classes?.items as Record<string, never>) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
70
src/components/widgets/Features3.astro
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import ItemGrid from '~/components/ui/ItemGrid.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import type { Features as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
image,
|
||||||
|
items = [],
|
||||||
|
columns,
|
||||||
|
defaultIcon,
|
||||||
|
isBeforeContent,
|
||||||
|
isAfterContent,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper
|
||||||
|
id={id}
|
||||||
|
isDark={isDark}
|
||||||
|
containerClass={`${isBeforeContent ? 'md:pb-8 lg:pb-12' : ''} ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${
|
||||||
|
classes?.container ?? ''
|
||||||
|
}`}
|
||||||
|
bg={bg}
|
||||||
|
>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
|
||||||
|
<div aria-hidden="true" class="aspect-w-16 aspect-h-7">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg"
|
||||||
|
width="auto"
|
||||||
|
height={320}
|
||||||
|
widths={[400, 768]}
|
||||||
|
layout="fullWidth"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: 'mt-12',
|
||||||
|
panel: 'max-w-full sm:max-w-md',
|
||||||
|
title: 'text-lg font-semibold',
|
||||||
|
description: 'mt-0.5',
|
||||||
|
icon: 'flex-shrink-0 mt-1 text-primary w-6 h-6',
|
||||||
|
...((classes?.items as object) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
104
src/components/widgets/Footer.astro
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
import { getHomePermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
text?: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Links {
|
||||||
|
title?: string;
|
||||||
|
links: Array<Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
links: Array<Links>;
|
||||||
|
secondaryLinks: Array<Link>;
|
||||||
|
socialLinks: Array<Link>;
|
||||||
|
footNote?: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme = 'light' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
|
||||||
|
<div class="dark:bg-dark absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div
|
||||||
|
class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300 intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
|
||||||
|
<div class="col-span-12 lg:col-span-4">
|
||||||
|
<div class="mb-2">
|
||||||
|
<a class="inline-block font-bold text-xl" href={getHomePermalink()}>{SITE?.name}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted flex gap-1">
|
||||||
|
{
|
||||||
|
secondaryLinks.map(({ text, href }, index) => (
|
||||||
|
<>
|
||||||
|
{index !== 0 ? ' · ' : ''}
|
||||||
|
<a
|
||||||
|
class="text-muted hover:text-gray-700 dark:text-gray-400 hover:underline transition duration-150 ease-in-out"
|
||||||
|
href={href}
|
||||||
|
set:html={text}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
links.map(({ title, links }) => (
|
||||||
|
<div class="col-span-6 md:col-span-3 lg:col-span-2">
|
||||||
|
<div class="dark:text-gray-300 font-medium mb-2">{title}</div>
|
||||||
|
{links && Array.isArray(links) && links.length > 0 && (
|
||||||
|
<ul class="text-sm">
|
||||||
|
{links.map(({ text, href, ariaLabel }) => (
|
||||||
|
<li class="mb-2">
|
||||||
|
<a
|
||||||
|
class="text-muted hover:text-gray-700 hover:underline dark:text-gray-400 transition duration-150 ease-in-out"
|
||||||
|
href={href}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="md:flex md:items-center md:justify-between py-6 md:py-8">
|
||||||
|
{
|
||||||
|
socialLinks?.length ? (
|
||||||
|
<ul class="flex mb-4 md:order-1 -ml-2 md:ml-4 md:mb-0 rtl:ml-0 rtl:-mr-2 rtl:md:ml-0 rtl:md:mr-4">
|
||||||
|
{socialLinks.map(({ ariaLabel, href, text, icon }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5" />}
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="text-sm mr-4 dark:text-muted">
|
||||||
|
<Fragment set:html={footNote} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
167
src/components/widgets/Header.astro
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import Logo from '~/components/Logo.astro';
|
||||||
|
import ToggleTheme from '~/components/common/ToggleTheme.astro';
|
||||||
|
import ToggleMenu from '~/components/common/ToggleMenu.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import { getHomePermalink } from '~/utils/permalinks';
|
||||||
|
import { trimSlash, getAsset } from '~/utils/permalinks';
|
||||||
|
import type { CallToAction } from '~/types';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
text?: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuLink extends Link {
|
||||||
|
links?: Array<MenuLink>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
id?: string;
|
||||||
|
links?: Array<MenuLink>;
|
||||||
|
actions?: Array<CallToAction>;
|
||||||
|
isSticky?: boolean;
|
||||||
|
isDark?: boolean;
|
||||||
|
isFullWidth?: boolean;
|
||||||
|
showToggleTheme?: boolean;
|
||||||
|
showRssFeed?: boolean;
|
||||||
|
position?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id = 'header',
|
||||||
|
links = [],
|
||||||
|
actions = [],
|
||||||
|
isSticky = false,
|
||||||
|
isDark = false,
|
||||||
|
isFullWidth = false,
|
||||||
|
showToggleTheme = false,
|
||||||
|
showRssFeed = false,
|
||||||
|
position = 'center',
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
class:list={[
|
||||||
|
{ sticky: isSticky, relative: !isSticky, dark: isDark },
|
||||||
|
'top-0 z-40 flex-none mx-auto w-full border-b border-gray-50/0 transition-[opacity] ease-in-out',
|
||||||
|
]}
|
||||||
|
{...isSticky ? { 'data-aw-sticky-header': true } : {}}
|
||||||
|
{...id ? { id } : {}}
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0"></div>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
'relative text-default py-3 px-3 md:px-6 mx-auto w-full',
|
||||||
|
{
|
||||||
|
'md:flex md:justify-between': position !== 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'md:grid md:grid-cols-3 md:items-center': position === 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'max-w-7xl': !isFullWidth,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class:list={[{ 'mr-auto rtl:mr-0 rtl:ml-auto': position === 'right' }, 'flex justify-between']}>
|
||||||
|
<a class="flex items-center" href={getHomePermalink()}>
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center md:hidden">
|
||||||
|
<ToggleMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="flex flex-col md:flex-row md:self-center w-full md:w-auto text-xl md:text-[0.9375rem] tracking-[0.01rem] font-medium md:justify-center"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
links.map(({ text, href, links }) => (
|
||||||
|
<li class={links?.length ? 'dropdown' : ''}>
|
||||||
|
{links?.length ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hover:text-link dark:hover:text-white px-4 py-3 flex items-center whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{text}{' '}
|
||||||
|
<Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu md:backdrop-blur-md dark:md:bg-dark rounded md:absolute pl-4 md:pl-0 md:hidden font-medium md:bg-white/90 md:min-w-[200px] drop-shadow-xl">
|
||||||
|
{links.map(({ text: text2, href: href2 }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
'first:rounded-t last:rounded-b md:hover:bg-gray-100 hover:text-link dark:hover:text-white dark:hover:bg-gray-700 py-2 px-5 block whitespace-no-wrap',
|
||||||
|
{ 'aw-link-active': href2 === currentPath },
|
||||||
|
]}
|
||||||
|
href={href2}
|
||||||
|
>
|
||||||
|
{text2}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
'hover:text-link dark:hover:text-white px-4 py-3 flex items-center whitespace-nowrap',
|
||||||
|
{ 'aw-link-active': href === currentPath },
|
||||||
|
]}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
{ 'ml-auto rtl:ml-0 rtl:mr-auto': position === 'left' },
|
||||||
|
'hidden md:self-center md:flex items-center md:mb-0 fixed w-full md:w-auto md:static justify-end left-0 rtl:left-auto rtl:right-0 bottom-0 p-3 md:p-0 md:justify-self-end',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class="items-center flex justify-between w-full md:w-auto">
|
||||||
|
<div class="flex">
|
||||||
|
{showToggleTheme && <ToggleTheme iconClass="w-6 h-6 md:w-5 md:h-5 md:inline-block" />}
|
||||||
|
{
|
||||||
|
showRssFeed && (
|
||||||
|
<a
|
||||||
|
class="text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center"
|
||||||
|
aria-label="RSS Feed"
|
||||||
|
href={getAsset('/rss.xml')}
|
||||||
|
>
|
||||||
|
<Icon name="tabler:rss" class="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
actions?.length ? (
|
||||||
|
<span class="ml-4 rtl:ml-0 rtl:mr-4">
|
||||||
|
{actions.map((btnProps) => (
|
||||||
|
<Button {...btnProps} class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto" />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
81
src/components/widgets/Hero.astro
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import type { Hero as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : null}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20">
|
||||||
|
<div class="text-center pb-10 md:pb-16 max-w-5xl mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-4xl md:text-4xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
{
|
||||||
|
subtitle && (
|
||||||
|
<p
|
||||||
|
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={subtitle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="intersect-once intercept-no-queue intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade flex justify-center"
|
||||||
|
>
|
||||||
|
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d613.1422525811341!2d4.675828277548104!3d52.06147025524555!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47c5d0e3f7fe9653%3A0x96c053935b9909a3!2sElzenbroek%204%2C%202811%20NX%20Reeuwijk!5e0!3m2!1sen!2snl!4v1729865646290!5m2!1sen!2snl" width="1000" height="600" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
97
src/components/widgets/Hero2.astro
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import type { Hero as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : null}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="py-12 md:py-20 lg:py-0 lg:flex lg:items-center lg:h-screen lg:gap-8">
|
||||||
|
<div class="basis-1/2 text-center lg:text-left pb-10 md:pb-16 mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||||
|
{
|
||||||
|
subtitle && (
|
||||||
|
<p
|
||||||
|
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
|
||||||
|
set:html={subtitle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
<div class="basis-1/2">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-5xl intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto rounded-md w-full"
|
||||||
|
widths={[400, 768, 1024, 2040]}
|
||||||
|
sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px"
|
||||||
|
loading="eager"
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
77
src/components/widgets/HeroBlanc.astro
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import type { Hero as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : null}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="pt-12 md:pt-20">
|
||||||
|
<div class="text-center max-w-5xl mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-4xl md:text-4xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
{
|
||||||
|
subtitle && (
|
||||||
|
<p
|
||||||
|
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={subtitle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
86
src/components/widgets/HeroText.astro
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
import type { CallToAction } from '~/types';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
content?: string;
|
||||||
|
callToAction?: string | CallToAction;
|
||||||
|
callToAction2?: string | CallToAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
callToAction = await Astro.slots.render('callToAction'),
|
||||||
|
callToAction2 = await Astro.slots.render('callToAction2'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose">
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20 pb-8 md:pb-8">
|
||||||
|
<div class="text-center max-w-5xl mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
{
|
||||||
|
subtitle && (
|
||||||
|
<p
|
||||||
|
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
set:html={subtitle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
{typeof callToAction === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction} />
|
||||||
|
) : (
|
||||||
|
<Button variant="primary" {...callToAction} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
callToAction2 && (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
{typeof callToAction2 === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction2} />
|
||||||
|
) : (
|
||||||
|
<Button {...callToAction2} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
11
src/components/widgets/Note.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="bg-blue-50 dark:bg-slate-800 not-prose">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-4 text-md text-center font-medium">
|
||||||
|
<span class="font-bold">
|
||||||
|
<Icon name="tabler:info-square" class="w-5 h-5 inline-block align-text-bottom" /> Philosophy:</span
|
||||||
|
> Simplicity, Best Practices and High Performance
|
||||||
|
</div>
|
||||||
|
</section>
|
86
src/components/widgets/Pricing.astro
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Pricing as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
prices = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<div class="flex flex-col justify-stretch items-center w-max mx-auto">
|
||||||
|
<div class="grid grid-cols-3 gap-4 dark:text-white sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
|
||||||
|
{
|
||||||
|
prices &&
|
||||||
|
prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => (
|
||||||
|
<div class="col-span-3 mx-auto flex w-full sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-1 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
|
||||||
|
{price && period && (
|
||||||
|
<div class="rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow px-6 py-8 flex w-full max-w-sm flex-col justify-between text-center">
|
||||||
|
{hasRibbon && ribbonTitle && (
|
||||||
|
<div class="absolute right-[-5px] 2xl:right-[-8px] rtl:right-auto rtl:left-[-8px] rtl:2xl:left-[-10px] top-[-5px] 2xl:top-[-10px] z-[1] h-[100px] w-[100px] overflow-hidden text-right">
|
||||||
|
<span class="absolute top-[19px] right-[-21px] rtl:right-auto rtl:left-[-21px] block w-full rotate-45 rtl:-rotate-45 bg-green-700 text-center text-[10px] font-bold uppercase leading-5 text-white shadow-[0_3px_10px_-5px_rgba(0,0,0,0.3)] before:absolute before:left-0 before:top-full before:z-[-1] before:border-[3px] before:border-r-transparent before:border-b-transparent before:border-l-green-800 before:border-t-green-800 before:content-[''] after:absolute after:right-0 after:top-full after:z-[-1] after:border-[3px] after:border-l-transparent after:border-b-transparent after:border-r-green-800 after:border-t-green-800 after:content-['']">
|
||||||
|
{ribbonTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="px-2 py-0">
|
||||||
|
{title && (
|
||||||
|
<h3 class="text-center text-md font-semibold uppercase leading-6 tracking-wider mb-2">{title}</h3>
|
||||||
|
)}
|
||||||
|
<div class="font-light sm:text-lg text-gray-600 dark:text-slate-400"><Fragment set:html={subtitle} /></div>
|
||||||
|
<div class="mb-8 mt-4">
|
||||||
|
<div class="flex items-center justify-center text-center mb-1">
|
||||||
|
<span class="text-4xl font-extrabold">{price}<sup>*</sup></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-base leading-6 lowercase text-gray-600 dark:text-slate-400">{period}</span>
|
||||||
|
</div>
|
||||||
|
{items && (
|
||||||
|
<ul class="my-8 md:my-10 space-y-2 text-left">
|
||||||
|
{items.map(
|
||||||
|
({ description, icon }) =>
|
||||||
|
description && (
|
||||||
|
<li class="mb-1.5 flex items-start space-x-3 leading-7">
|
||||||
|
<div class="rounded-full bg-primary mt-1">
|
||||||
|
<Icon name={icon ? icon : 'tabler:check'} class="w-5 h-5 font-bold p-1 text-white" />
|
||||||
|
</div>
|
||||||
|
<span>{description}</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{callToAction && (
|
||||||
|
<div class={`flex justify-center`}>
|
||||||
|
{typeof callToAction === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction} />
|
||||||
|
) : (
|
||||||
|
callToAction &&
|
||||||
|
callToAction.href && <Button {...(hasRibbon ? { variant: 'primary' } : {})} {...callToAction} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="w-full mt-8 text-sm">
|
||||||
|
<sup>*</sup> De prijzen zijn excl BTW.<br>
|
||||||
|
<sup>**</sup> Afwijken van de tijden is mogelijk in overleg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
46
src/components/widgets/Stats.astro
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import type { Stats as Props } from '~/types';
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import Headline from '../ui/Headline.astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
stats = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<div class="flex flex-wrap justify-center -m-4 text-center">
|
||||||
|
{
|
||||||
|
stats &&
|
||||||
|
stats.map(({ amount, title, icon }) => (
|
||||||
|
<div class="p-4 md:w-1/4 sm:w-1/2 w-full min-w-[220px] text-center md:border-r md:last:border-none dark:md:border-slate-500 intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade intersect-quarter">
|
||||||
|
{icon && (
|
||||||
|
<div class="flex items-center justify-center mx-auto mb-4 text-primary">
|
||||||
|
<Icon name={icon} class="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{amount && (
|
||||||
|
<div class="font-heading text-primary text-[2.6rem] font-bold dark:text-white lg:text-5xl xl:text-6xl">
|
||||||
|
{amount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<div class="text-sm font-medium uppercase tracking-widest text-gray-800 dark:text-slate-400 lg:text-base">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
59
src/components/widgets/Steps.astro
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Timeline from '~/components/ui/Timeline.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import type { Steps as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
isReversed = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div class:list={['flex flex-col gap-8 md:gap-12', { 'md:flex-row-reverse': isReversed }, { 'md:flex-row': image }]}>
|
||||||
|
<div class:list={['md:py-4 md:self-center', { 'md:basis-1/2': image }, { 'w-full': !image }]}>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'text-left rtl:text-right',
|
||||||
|
title: 'text-3xl lg:text-4xl',
|
||||||
|
...((classes?.headline as object) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Timeline items={items} classes={classes?.items as Record<string, never>} />
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative md:basis-1/2">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="inset-0 object-cover object-top w-full rounded-md shadow-lg md:absolute md:h-full bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 768]}
|
||||||
|
sizes="(max-width: 768px) 100vw, 432px"
|
||||||
|
width={432}
|
||||||
|
height={768}
|
||||||
|
layout="cover"
|
||||||
|
src={image?.src}
|
||||||
|
alt={image?.alt || ''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
72
src/components/widgets/Steps2.astro
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import type { Steps as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
callToAction = await Astro.slots.render('callToAction'),
|
||||||
|
items = [],
|
||||||
|
isReversed = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div class={`flex flex-col gap-8 md:gap-12 md:flex-row ${isReversed ? 'md:flex-row-reverse' : ''}`}>
|
||||||
|
<div class={`w-full lg:w-1/2 gap-8 md:gap-12 ${isReversed ? 'lg:ml-16 md:ml-8 ml-0' : 'lg:mr-16 md:mr-8 mr-0'}`}>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'text-center md:text-left rtl:md:text-right mb-4 md:mb-8',
|
||||||
|
title: 'mb-4 text-3xl lg:text-4xl font-bold font-heading',
|
||||||
|
subtitle: 'mb-8 text-xl text-muted dark:text-slate-400',
|
||||||
|
// ...((classes?.headline as {}) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="w-full text-center md:text-left rtl:md:text-right">
|
||||||
|
{
|
||||||
|
typeof callToAction === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction} />
|
||||||
|
) : (
|
||||||
|
callToAction &&
|
||||||
|
callToAction.text &&
|
||||||
|
callToAction.href && <Button variant="primary" {...callToAction} class="mb-12 w-auto" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full lg:w-1/2 px-0">
|
||||||
|
<ul class="space-y-10">
|
||||||
|
{
|
||||||
|
items && items.length
|
||||||
|
? items.map(({ title: title2, description, icon }, index) => (
|
||||||
|
<li class="flex md:-mx-4">
|
||||||
|
<div class="pr-4 sm:pl-4 rtl:pr-0 rtl:pl-4 rtl:sm:pl-0 rtl:sm:pr-4">
|
||||||
|
<span class="flex w-16 h-16 mx-auto items-center justify-center text-2xl font-bold rounded-full bg-blue-100 text-primary">
|
||||||
|
{icon ? <Icon name={icon} class="w-6 h-6 icon-bold" /> : index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pl-4 rtl:pl-0 rtl:pr-4">
|
||||||
|
<h3 class="mb-4 text-xl font-semibold font-heading" set:html={title2} />
|
||||||
|
<p class="text-muted dark:text-gray-400" set:html={description} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
75
src/components/widgets/Testimonials.astro
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import type { Testimonials as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
testimonials = [],
|
||||||
|
callToAction,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{
|
||||||
|
testimonials &&
|
||||||
|
testimonials.map(({ title, testimonial, name, job, image }) => (
|
||||||
|
<div class="flex h-auto intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
|
||||||
|
<div class="flex flex-col p-4 md:p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600">
|
||||||
|
{title && <h2 class="text-lg font-medium leading-6 pb-4">{title}</h2>}
|
||||||
|
{testimonial && (
|
||||||
|
<blockquote class="flex-auto">
|
||||||
|
<p class="text-muted">" {testimonial} "</p>
|
||||||
|
</blockquote>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr class="border-slate-200 dark:border-slate-600 my-4" />
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
{image && (
|
||||||
|
<div class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600 min-w-full min-h-full"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
widths={[400, 768]}
|
||||||
|
layout="fixed"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="grow ml-3 rtl:ml-0 rtl:mr-3">
|
||||||
|
{name && <p class="text-base font-semibold">{name}</p>}
|
||||||
|
{job && <p class="text-xs text-muted">{job}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="flex justify-center mx-auto w-fit mt-8 md:mt-12 font-medium">
|
||||||
|
<Button {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</WidgetWrapper>
|
67
src/config.yaml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
site:
|
||||||
|
name: Bij Lobke!
|
||||||
|
site: 'https://astrowind.vercel.app'
|
||||||
|
base: '/'
|
||||||
|
trailingSlash: false
|
||||||
|
|
||||||
|
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
|
||||||
|
|
||||||
|
# Default SEO metadata
|
||||||
|
metadata:
|
||||||
|
title:
|
||||||
|
default: Bij Lobke
|
||||||
|
template: '%s — Bij Lobke'
|
||||||
|
description: "Vergaderen in een authentieke kroeg, in een landelijke en rustgevende omgeving"
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
follow: true
|
||||||
|
openGraph:
|
||||||
|
site_name: BijLobke
|
||||||
|
images:
|
||||||
|
- url: '~/assets/images/binnen.webp'
|
||||||
|
width: 1200
|
||||||
|
height: 628
|
||||||
|
type: website
|
||||||
|
twitter:
|
||||||
|
handle: '@onwidget'
|
||||||
|
site: '@onwidget'
|
||||||
|
cardType: summary_large_image
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
language: nl
|
||||||
|
textDirection: ltr
|
||||||
|
|
||||||
|
apps:
|
||||||
|
blog:
|
||||||
|
isEnabled: false
|
||||||
|
postsPerPage: 6
|
||||||
|
|
||||||
|
post:
|
||||||
|
isEnabled: false
|
||||||
|
permalink: '/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
list:
|
||||||
|
isEnabled: false
|
||||||
|
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
category:
|
||||||
|
isEnabled: false
|
||||||
|
pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
tag:
|
||||||
|
isEnabled: false
|
||||||
|
pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
|
||||||
|
robots:
|
||||||
|
index: false
|
||||||
|
|
||||||
|
isRelatedPostsEnabled: false
|
||||||
|
relatedPostsCount: 4
|
||||||
|
|
||||||
|
ui:
|
||||||
|
theme: 'light:only' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
|
5
src/env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="../vendor/integration/types.d.ts" />
|
35
src/layouts/LandingLayout.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '~/layouts/PageLayout.astro';
|
||||||
|
import Header from '~/components/widgets/Header.astro';
|
||||||
|
|
||||||
|
import { headerData } from '~/navigation';
|
||||||
|
import type { MetaData } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
metadata?: MetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout metadata={metadata}>
|
||||||
|
<Fragment slot="announcement">
|
||||||
|
<slot name="announcement" />
|
||||||
|
</Fragment>
|
||||||
|
<Fragment slot="header">
|
||||||
|
<slot name="header">
|
||||||
|
<Header
|
||||||
|
links={headerData?.links[2] ? [headerData.links[2]] : undefined}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: 'Download',
|
||||||
|
href: 'https://github.com/onwidget/astrowind',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
showToggleTheme
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</Fragment>
|
||||||
|
<slot />
|
||||||
|
</PageLayout>
|
48
src/layouts/Layout.astro
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
import '~/assets/styles/tailwind.css';
|
||||||
|
|
||||||
|
import { I18N } from 'astrowind:config';
|
||||||
|
|
||||||
|
import CommonMeta from '~/components/common/CommonMeta.astro';
|
||||||
|
import Favicons from '~/components/Favicons.astro';
|
||||||
|
import CustomStyles from '~/components/CustomStyles.astro';
|
||||||
|
import ApplyColorMode from '~/components/common/ApplyColorMode.astro';
|
||||||
|
import Metadata from '~/components/common/Metadata.astro';
|
||||||
|
import SiteVerification from '~/components/common/SiteVerification.astro';
|
||||||
|
import Analytics from '~/components/common/Analytics.astro';
|
||||||
|
import BasicScripts from '~/components/common/BasicScripts.astro';
|
||||||
|
|
||||||
|
// Comment the line below to disable View Transitions
|
||||||
|
import { ViewTransitions } from 'astro:transitions';
|
||||||
|
|
||||||
|
import type { MetaData as MetaDataType } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
metadata?: MetaDataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata = {} } = Astro.props;
|
||||||
|
const { language, textDirection } = I18N;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={language} dir={textDirection} class="2xl:text-[20px]">
|
||||||
|
<head>
|
||||||
|
<CommonMeta />
|
||||||
|
<Favicons />
|
||||||
|
<CustomStyles />
|
||||||
|
<ApplyColorMode />
|
||||||
|
<Metadata {...metadata} />
|
||||||
|
<SiteVerification />
|
||||||
|
<Analytics />
|
||||||
|
|
||||||
|
<!-- Comment the line below to disable View Transitions -->
|
||||||
|
<ViewTransitions fallback="swap" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="antialiased text-default bg-page tracking-tight">
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<BasicScripts />
|
||||||
|
</body>
|
||||||
|
</html>
|