new
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
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.mjs
|
||||
└── ...
|
||||
```
|
||||
|
||||
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 format` | Format codes with Prettier |
|
||||
| `npm run lint:eslint` | Run Eslint |
|
||||
| `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.
|
98
astro.config.mjs
Normal file
@ -0,0 +1,98 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { defineConfig, squooshImageService } 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 astrowind from './vendor/integration';
|
||||
|
||||
import {
|
||||
readingTimeRemarkPlugin,
|
||||
responsiveTablesRehypePlugin,
|
||||
lazyImagesRehypePlugin,
|
||||
} from './src/utils/frontmatter.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const hasExternalScripts = false;
|
||||
const whenExternalScripts = (items = []) =>
|
||||
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',
|
||||
'globe',
|
||||
'positive-dynamic',
|
||||
'flash-on',
|
||||
'collect',
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
...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',
|
||||
}),
|
||||
],
|
||||
|
||||
image: {
|
||||
service: squooshImageService(),
|
||||
domains: ['cdn.pixabay.com'],
|
||||
},
|
||||
|
||||
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"
|
11470
package-lock.json
generated
Normal file
63
package.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@onwidget/astrowind",
|
||||
"version": "1.0.0-beta.38",
|
||||
"description": "AstroWind: A free template using Astro 4.0 and Tailwind CSS. Astro starter theme.",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"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",
|
||||
"format": "prettier -w .",
|
||||
"lint:eslint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^4.0.6",
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"@astrolib/analytics": "^0.5.0",
|
||||
"@astrolib/seo": "^1.0.0-beta.5",
|
||||
"@fontsource-variable/inter": "^5.0.18",
|
||||
"astro": "^4.11.0",
|
||||
"astro-embed": "^0.7.2",
|
||||
"astro-icon": "^1.1.0",
|
||||
"limax": "4.1.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"unpic": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/mdx": "^3.1.1",
|
||||
"@astrojs/partytown": "^2.1.1",
|
||||
"@astrojs/tailwind": "5.1.0",
|
||||
"@eslint/js": "^9.5.0",
|
||||
"@iconify-json/flat-color-icons": "^1.1.10",
|
||||
"@iconify-json/tabler": "^1.1.114",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"astro-compress": "2.2.28",
|
||||
"astro-eslint-parser": "^1.0.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-astro": "^1.2.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"globals": "^15.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-astro": "^0.14.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"rehype-plugin-image-native-lazy-loading": "^1.2.0",
|
||||
"sharp": "0.33.4",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.5.2",
|
||||
"typescript-eslint": "^7.13.1"
|
||||
}
|
||||
}
|
2
public/_headers
Normal file
@ -0,0 +1,2 @@
|
||||
/_astro/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
29
public/decapcms/config.yml
Normal file
@ -0,0 +1,29 @@
|
||||
backend:
|
||||
name: git-gateway
|
||||
branch: main
|
||||
|
||||
media_folder: 'src/assets/images'
|
||||
public_folder: '/_astro'
|
||||
|
||||
collections:
|
||||
- name: 'post'
|
||||
label: 'Post'
|
||||
folder: 'src/content/post'
|
||||
create: true
|
||||
fields:
|
||||
- { label: 'Title', name: 'title', widget: 'string' }
|
||||
- { label: 'Excerpt', name: 'excerpt', widget: 'string' }
|
||||
- { label: 'Category', name: 'category', widget: 'string' }
|
||||
- {
|
||||
label: 'Tags',
|
||||
name: 'tags',
|
||||
widget: 'list',
|
||||
allow_add: true,
|
||||
allow_delete: true,
|
||||
collapsed: false,
|
||||
field: { label: 'Tag', name: 'tag', widget: 'string' },
|
||||
}
|
||||
- { label: 'Image', name: 'image', widget: 'string' }
|
||||
- { label: 'Publish Date', name: 'publishDate', widget: 'datetime', required: false }
|
||||
- { label: 'Author', name: 'author', widget: 'string' }
|
||||
- { label: 'Content', name: 'body', widget: 'markdown' }
|
14
public/decapcms/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Content Manager</title>
|
||||
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Include the script that builds the page and powers Decap CMS -->
|
||||
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
||||
</body>
|
||||
</html>
|
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: 9.4 KiB |
BIN
src/assets/favicons/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
2
src/assets/favicons/favicon.svg
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/images/africanregensummit.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/images/app-store.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/bg.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
1
src/assets/images/bg.svg
Normal file
After Width: | Height: | Size: 143 KiB |
BIN
src/assets/images/default.png
Normal file
After Width: | Height: | Size: 563 KiB |
BIN
src/assets/images/digitalfreezonezanzibar.png
Normal file
After Width: | Height: | Size: 490 KiB |
BIN
src/assets/images/google-play.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/hero-image.png
Normal file
After Width: | Height: | Size: 539 KiB |
BIN
src/assets/images/inthenews.png
Normal file
After Width: | Height: | Size: 683 KiB |
1
src/assets/images/logo.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/images/newchapter.png
Normal file
After Width: | Height: | Size: 965 KiB |
1
src/assets/images/ow.svg
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/assets/images/photo-1504384308090-c894fdcc538d.avif
Normal file
BIN
src/assets/images/simple.png
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
src/assets/images/zanzibar.png
Normal file
After Width: | Height: | Size: 3.2 MiB |
97
src/assets/styles/tailwind.css
Normal file
@ -0,0 +1,97 @@
|
||||
@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);
|
||||
}
|
||||
.bg-grad {
|
||||
--tw-bg-opacity: 1;
|
||||
background: rgb(255,255,255);
|
||||
background: radial-gradient(circle, rgba(255,255,255,1) 0%, rgba(182,189,255,0.15312062324929976) 100%);
|
||||
}
|
||||
.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;
|
||||
}
|
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: 'Poppins';
|
||||
--aw-font-serif: 'Poppins';
|
||||
--aw-font-heading: 'Poppins';
|
||||
|
||||
--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(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: 'Poppins';
|
||||
--aw-font-serif: 'Poppins';
|
||||
--aw-font-heading: 'Poppins';
|
||||
|
||||
--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} />
|
8
src/components/Logo.astro
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
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">
|
||||
<img src="/src/assets/images/logo.svg" alt="Logo" class="inline-block w-6 h-6 mr-2">
|
||||
{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>
|
69
src/components/blog/GridItem.astro
Normal file
@ -0,0 +1,69 @@
|
||||
---
|
||||
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">
|
||||
<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>
|
23
src/components/blog/List.astro
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
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>
|
||||
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
|
||||
<div class="aspect-[1108/632] w-[69.25rem] bg-[#80caff] opacity-10" style="clip-path: polygon(26.4% 51.7%, 8.3% 11.8%, 0% 46.4%, 2.6% 82.2%, 7.5% 84.9%, 24.3% 64%, 44.7% 47.5%, 53.5% 49.4%, 55% 62.9%, 49.7% 87.2%, 78.7% 64.1%, 99.9% 100%, 94.6% 51.1%, 78.6% 63.9%, 41.1% 0.2%, 26.4% 51.7%)"></div>
|
||||
</div>
|
118
src/components/blog/ListItem.astro
Normal file
@ -0,0 +1,118 @@
|
||||
---
|
||||
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 ${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>
|
||||
)
|
||||
}
|
28
src/components/blog/RelatedPosts.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
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' }}
|
||||
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">
|
||||
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
|
||||
<div class="aspect-[1108/632] w-[69.25rem] bg-[#80caff] opacity-10" style="clip-path: polygon(26.4% 51.7%, 8.3% 11.8%, 0% 46.4%, 2.6% 82.2%, 7.5% 84.9%, 24.3% 64%, 44.7% 47.5%, 53.5% 49.4%, 55% 62.9%, 49.7% 87.2%, 78.7% 64.1%, 99.9% 100%, 94.6% 51.1%, 78.6% 63.9%, 41.1% 0.2%, 26.4% 51.7%)"></div>
|
||||
</div>
|
||||
<article>
|
||||
<header class={post.image ? '' : ''}>
|
||||
<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>
|
162
src/components/common/BasicScripts.astro
Normal file
@ -0,0 +1,162 @@
|
||||
---
|
||||
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>
|
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')} />
|
64
src/components/common/Image.astro
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
import { findImage } from '~/utils/images';
|
||||
import {
|
||||
getImagesOptimized,
|
||||
astroAsseetsOptimizer,
|
||||
unpicOptimizer,
|
||||
isUnpicCompatible,
|
||||
type ImageProps,
|
||||
type AttributesProps,
|
||||
} from '~/utils/images-optimization';
|
||||
|
||||
type Props = ImageProps;
|
||||
type ImageType = {
|
||||
src: string;
|
||||
attributes: AttributesProps;
|
||||
};
|
||||
|
||||
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') {
|
||||
if ((_image.startsWith('http://') || _image.startsWith('https://')) && isUnpicCompatible(_image)) {
|
||||
image = await getImagesOptimized(_image, props, unpicOptimizer);
|
||||
} else {
|
||||
image = {
|
||||
src: _image,
|
||||
attributes: { ...props, src: undefined },
|
||||
};
|
||||
}
|
||||
} 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>
|
||||
<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>
|
||||
)
|
||||
}
|
53
src/components/ui/ItemGrid2.astro
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
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', 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>
|
||||
)
|
||||
}
|
54
src/components/ui/Timeline.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
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', 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>
|
||||
)
|
||||
}
|
31
src/components/ui/WidgetWrapper.astro
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
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', 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-4110/" class="text-muted hover:underline dark:text-slate-400 font-medium"
|
||||
>Astro 4.11 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>
|
43
src/components/widgets/Contact.astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
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>
|
||||
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
|
||||
<div class="aspect-[1108/632] w-[69.25rem] bg-[#80caff] opacity-10" style="clip-path: polygon(26.4% 51.7%, 8.3% 11.8%, 0% 46.4%, 2.6% 82.2%, 7.5% 84.9%, 24.3% 64%, 44.7% 47.5%, 53.5% 49.4%, 55% 62.9%, 49.7% 87.2%, 78.7% 64.1%, 99.9% 100%, 94.6% 51.1%, 78.6% 63.9%, 41.1% 0.2%, 26.4% 51.7%)"></div>
|
||||
</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>
|
41
src/components/widgets/Features.astro
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
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 ${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>) ?? {}),
|
||||
}}
|
||||
/>
|
||||
<div class="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]" aria-hidden="true">
|
||||
<div class="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-[#89CFF0] to-[#A2CFFE] opacity-30 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]" style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)"></div>
|
||||
</div>
|
||||
</WidgetWrapper>
|
41
src/components/widgets/Features0.astro
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||
import Headline from '~/components/ui/Headline.astro';
|
||||
import ItemGrid2 from '~/components/ui/ItemGrid2.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'),
|
||||
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>
|
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>
|
102
src/components/widgets/Footer.astro
Normal file
@ -0,0 +1,102 @@
|
||||
---
|
||||
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">
|
||||
<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>
|
166
src/components/widgets/Header.astro
Normal file
@ -0,0 +1,166 @@
|
||||
---
|
||||
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 ActionLink extends CallToAction {}
|
||||
|
||||
interface MenuLink extends Link {
|
||||
links?: Array<MenuLink>;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
links?: Array<MenuLink>;
|
||||
actions?: Array<ActionLink>;
|
||||
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">
|
||||
{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',
|
||||
{ '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>
|
99
src/components/widgets/Hero.astro
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
import Image from '~/components/common/Image.astro';
|
||||
import Button from '~/components/ui/Button.astro';
|
||||
import Background from '~/components/ui/Background.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-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]" aria-hidden="true">
|
||||
<div class="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-[#89CFF0] to-[#A2CFFE] opacity-30 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]" style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)"></div>
|
||||
</div>
|
||||
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
|
||||
<div class="aspect-[1108/632] w-[69.25rem] bg-[#80caff] opacity-10" style="clip-path: polygon(26.4% 51.7%, 8.3% 11.8%, 0% 46.4%, 2.6% 82.2%, 7.5% 84.9%, 24.3% 64%, 44.7% 47.5%, 53.5% 49.4%, 55% 62.9%, 49.7% 87.2%, 78.7% 64.1%, 99.9% 100%, 94.6% 51.1%, 78.6% 63.9%, 41.1% 0.2%, 26.4% 51.7%)"></div>
|
||||
</div>
|
||||
<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">
|
||||
<div>
|
||||
{
|
||||
image && (
|
||||
<div class="relative m-auto max-w-5xl">
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Image
|
||||
class="mx-auto rounded-md w-full"
|
||||
widths={[100, 192, 256, 510]}
|
||||
sizes="(max-width: 767px) 100px, (max-width: 1023px) 192px, (max-width: 2039px) 256px, 510px"
|
||||
loading="eager"
|
||||
width={256}
|
||||
height={256}
|
||||
{...image}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
tagline && (
|
||||
<p
|
||||
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" 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">
|
||||
{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>
|
95
src/components/widgets/Hero2.astro
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
import Image from '~/components/common/Image.astro';
|
||||
import Button from '~/components/ui/Button.astro';
|
||||
import Background from '~/components/ui/Background.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">
|
||||
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
|
||||
<div class="aspect-[1108/632] w-[69.25rem] bg-[#80caff] opacity-10" style="clip-path: polygon(26.4% 51.7%, 8.3% 11.8%, 0% 46.4%, 2.6% 82.2%, 7.5% 84.9%, 24.3% 64%, 44.7% 47.5%, 53.5% 49.4%, 55% 62.9%, 49.7% 87.2%, 78.7% 64.1%, 99.9% 100%, 94.6% 51.1%, 78.6% 63.9%, 41.1% 0.2%, 26.4% 51.7%)"></div>
|
||||
</div>
|
||||
<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 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"
|
||||
set:html={tagline}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
title && (
|
||||
<h1
|
||||
class="text-3xl md:text-5xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||
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" 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">
|
||||
{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">
|
||||
{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={600}
|
||||
height={600}
|
||||
{...image}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
77
src/components/widgets/HeroText.astro
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
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"
|
||||
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"
|
||||
set:html={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" 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">
|
||||
{
|
||||
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>
|
83
src/components/widgets/Pricing.astro
Normal file
@ -0,0 +1,83 @@
|
||||
---
|
||||
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 items-stretch justify-center">
|
||||
<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">
|
||||
{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-xl font-semibold uppercase leading-6 tracking-wider mb-2">{title}</h3>
|
||||
)}
|
||||
{subtitle && <p class="font-light sm:text-lg text-gray-600 dark:text-slate-400">{subtitle}</p>}
|
||||
<div class="my-8">
|
||||
<div class="flex items-center justify-center text-center mb-1">
|
||||
<span class="text-5xl">$</span>
|
||||
<span class="text-6xl font-extrabold">{price}</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>
|
||||
</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">
|
||||
{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>
|
75
src/components/widgets/Steps2.astro
Normal file
@ -0,0 +1,75 @@
|
||||
---
|
||||
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="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
|
||||
<div class="aspect-[1108/632] w-[69.25rem] bg-[#80caff] opacity-10" style="clip-path: polygon(26.4% 51.7%, 8.3% 11.8%, 0% 46.4%, 2.6% 82.2%, 7.5% 84.9%, 24.3% 64%, 44.7% 47.5%, 53.5% 49.4%, 55% 62.9%, 49.7% 87.2%, 78.7% 64.1%, 99.9% 100%, 94.6% 51.1%, 78.6% 63.9%, 41.1% 0.2%, 26.4% 51.7%)"></div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
72
src/config.yaml
Normal file
@ -0,0 +1,72 @@
|
||||
site:
|
||||
name: FreeZone
|
||||
site: 'https://astrowind.vercel.app'
|
||||
base: '/'
|
||||
trailingSlash: false
|
||||
|
||||
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
|
||||
|
||||
# Default SEO metadata
|
||||
metadata:
|
||||
title:
|
||||
default: FreeZone
|
||||
template: '%s — AstroWind'
|
||||
description: "\U0001F680 Suitable for Startups, Small Business, Sass Websites, Professional Portfolios, Marketing Websites, Landing Pages & Blogs."
|
||||
robots:
|
||||
index: true
|
||||
follow: true
|
||||
openGraph:
|
||||
site_name: FreeZone
|
||||
images:
|
||||
- url: '~/assets/images/default.png'
|
||||
width: 1200
|
||||
height: 628
|
||||
type: website
|
||||
twitter:
|
||||
handle: '@onwidget'
|
||||
site: '@onwidget'
|
||||
cardType: summary_large_image
|
||||
|
||||
i18n:
|
||||
language: en
|
||||
textDirection: ltr
|
||||
|
||||
apps:
|
||||
blog:
|
||||
isEnabled: true
|
||||
postsPerPage: 6
|
||||
|
||||
post:
|
||||
isEnabled: true
|
||||
permalink: '/%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
|
||||
relatedPostsCount: 4
|
||||
|
||||
analytics:
|
||||
vendors:
|
||||
googleAnalytics:
|
||||
id: null # or "G-XXXXXXXXXX"
|
||||
|
||||
ui:
|
||||
theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
|
68
src/content/config.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { z, defineCollection } from 'astro:content';
|
||||
|
||||
const metadataDefinition = () =>
|
||||
z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
ignoreTitleTemplate: z.boolean().optional(),
|
||||
|
||||
canonical: z.string().url().optional(),
|
||||
|
||||
robots: z
|
||||
.object({
|
||||
index: z.boolean().optional(),
|
||||
follow: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
description: z.string().optional(),
|
||||
|
||||
openGraph: z
|
||||
.object({
|
||||
url: z.string().optional(),
|
||||
siteName: z.string().optional(),
|
||||
images: z
|
||||
.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
locale: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
twitter: z
|
||||
.object({
|
||||
handle: z.string().optional(),
|
||||
site: z.string().optional(),
|
||||
cardType: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const postCollection = defineCollection({
|
||||
schema: z.object({
|
||||
publishDate: z.date().optional(),
|
||||
updateDate: z.date().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
|
||||
title: z.string(),
|
||||
excerpt: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
|
||||
category: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
author: z.string().optional(),
|
||||
|
||||
metadata: metadataDefinition(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
post: postCollection,
|
||||
};
|
26
src/content/post/A-New-Chapter-Unfolds.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
publishDate: 2023-08-23T00:00:00Z
|
||||
author: Sam Taggart
|
||||
title: 🌍 A New Chapter Unfolds - OurWorld Free Zone Joins Forces with Tanzania's Officials
|
||||
excerpt: OurWorld Free Zone Establishes Official Partnership with Zanzibar Government.
|
||||
image: ~/assets/images/newchapter.png
|
||||
category: News
|
||||
tags:
|
||||
- zanzibar
|
||||
- ODFZ
|
||||
metadata:
|
||||
canonical: "https://freezone.ourworld.tf/blog/a-new-chapter-unfolds/"
|
||||
---
|
||||
|
||||
OurWorld Free Zone is excited to announce a significant milestone in its journey towards global collaboration. On July 23 2023, a momentous contract was signed between OurWorld Free Zone and the government of Tanzania, solidifying their official partnership.
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
The signing ceremony, graced by the presence of OurWorld Free Zone's co-founder, Kristof De Spiegeleer, marked a pivotal moment in the organization's expansion efforts. The event was attended by a distinguished gathering, including members of the press, honored guests, and key government officials from both Zanzibar and Tanzania.
|
||||
During the event, discussions were held to outline the next steps in this groundbreaking collaboration. The partnership between OurWorld Free Zone and the Zanzibar government aims to pave the way for new opportunities and mutual growth. The cooperative efforts are poised to foster economic development, innovation, and cooperation between different sectors.
|
||||
|
||||
<br/>
|
||||
|
||||
Stay tuned for more detailed updates on this exciting partnership as OurWorld Free Zone and the government of Zanzibar work together to shape a promising future of collaboration and progress. Further announcements will shed light on the specific initiatives and projects that will emerge from this alliance.
|
28
src/content/post/a-digital-freezone.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
publishDate: 2023-07-23T00:00:00Z
|
||||
author: Sam Taggart
|
||||
title: 📣 A Digital Free Zone in Zanzibar – And More!
|
||||
excerpt: The President of Zanzibar, Dr. Hussein Mwinyi, visited us this weekend to publicly share our collaborations.
|
||||
image: ~/assets/images/digitalfreezonezanzibar.png
|
||||
category: News
|
||||
tags:
|
||||
- zanzibar
|
||||
- ODFZ
|
||||
- Partnership
|
||||
metadata:
|
||||
canonical: "https://freezone.ourworld.tf/blog/a-digital-freezone/"
|
||||
---
|
||||
|
||||
This past weekend, we were honored with a visit from Zanzibar's President Dr. Hussein Mwinyi at the African Regenerative Cities Summit.
|
||||
|
||||
<br/>
|
||||
|
||||
There, he formally and publicly expressed the government's commitment to the establishment of a digital free zone, a locally-owned Internet infrastructure, an incubator to support young local innovators – all in collaboration with ThreeFold – and their commitment to the ICT sector as a whole.
|
||||
|
||||
<br/>
|
||||
|
||||
Read Zanzibar Seeks to Become a Digital FreeZone (via The Citizen) and Mwinyi Commits to Improvement of Digital Spaces (via Daily News Tanzania) for further details.
|
||||
<br/>
|
||||
|
||||
We'll be sharing more developments on this front in the community call next week and as progress unfolds.
|
||||
|
27
src/content/post/african-regenerative-summit.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
publishDate: 2023-07-21T00:00:00Z
|
||||
author: Sam Taggart
|
||||
title: 🌍 African Regenerative Cities Summit
|
||||
excerpt: Learn about the summit we hosted this past weekend in Zanzibar to unite innovators and investors to support Africa's future.
|
||||
image: ~/assets/images/africanregensummit.png
|
||||
category: News
|
||||
tags:
|
||||
- zanzibar
|
||||
- ODFZ
|
||||
- Events
|
||||
metadata:
|
||||
canonical: "https://freezone.ourworld.tf/blog/african-regenerative-summit/"
|
||||
---
|
||||
|
||||
Over the past four days, we have had the pleasure to host the African Regenerative Cities Summit in Zanzibar, Tanzania – uniting top innovators and investors supporting human flourishing, with a special focus on science and technology that will lay the foundation for Africa’s future.
|
||||
|
||||
<br/>
|
||||
|
||||
A lot has happened over the course of the summit, including a surprise visit today from an extremely special guest.
|
||||
|
||||
<br/>
|
||||
|
||||
Generally, we have been blown away by the participants and the projects they are working on, and we warmly welcome many of them now into the ThreeFold community.
|
||||
<br/>
|
||||
|
||||
We'll be sharing more in the coming days.
|
19
src/content/post/in-the-news.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
publishDate: 2023-08-01T00:00:00Z
|
||||
author: Sam Taggart
|
||||
title: In the News – OurWorld Digital Free Zone
|
||||
excerpt: OurWorld Digital Free Zone received some coverage recently from IPPMEDIA. Take a look!
|
||||
image: ~/assets/images/inthenews.png
|
||||
category: News
|
||||
tags:
|
||||
- zanzibar
|
||||
- ODFZ
|
||||
metadata:
|
||||
canonical: "https://freezone.ourworld.tf/blog/in-the-news/"
|
||||
---
|
||||
|
||||
"Our vision is to foster a digital ecosystem that harmoniously blends the tranquility of Zanzibar with the awe-inspiring power of the digital age. I am deeply appreciative of the dedication of the OurWorld Zanzibar team in turning this vision into a reality," Zanzibar President Dr. Hussein Mwinyi.
|
||||
|
||||
<br/>
|
||||
|
||||
Take a look at this piece from IPPMEDIA, which goes into some detail about our collaboration with the Revolutionary Government of Zanzibar and the OurWorld Digital Free Zone.
|
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>
|
54
src/layouts/Layout.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
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 />
|
||||
|
||||
<style is:global>
|
||||
img {
|
||||
content-visibility: auto;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
28
src/layouts/MarkdownLayout.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import Layout from '~/layouts/PageLayout.astro';
|
||||
|
||||
import type { MetaData } from '~/types';
|
||||
|
||||
export interface Props {
|
||||
frontmatter: {
|
||||
title?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { frontmatter } = Astro.props;
|
||||
|
||||
const metadata: MetaData = {
|
||||
title: frontmatter?.title,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout metadata={metadata}>
|
||||
<section class="px-4 py-16 sm:px-6 mx-auto lg:px-8 lg:py-20 max-w-4xl">
|
||||
<h1 class="font-bold font-heading text-4xl md:text-5xl leading-tighter tracking-tighter">{frontmatter.title}</h1>
|
||||
<div
|
||||
class="mx-auto prose prose-lg max-w-4xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
29
src/layouts/PageLayout.astro
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
import Layout from '~/layouts/Layout.astro';
|
||||
import Header from '~/components/widgets/Header.astro';
|
||||
import Footer from '~/components/widgets/Footer.astro';
|
||||
import Announcement from '~/components/widgets/Announcement.astro';
|
||||
|
||||
import { headerData, footerData } from '~/navigation';
|
||||
|
||||
import type { MetaData } from '~/types';
|
||||
|
||||
export interface Props {
|
||||
metadata?: MetaData;
|
||||
}
|
||||
|
||||
const { metadata } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout metadata={metadata}>
|
||||
|
||||
<slot name="header">
|
||||
<Header {...headerData} isSticky showRssFeed showToggleTheme />
|
||||
</slot>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<slot name="footer">
|
||||
<Footer {...footerData} />
|
||||
</slot>
|
||||
</Layout>
|
99
src/navigation.js
Normal file
@ -0,0 +1,99 @@
|
||||
import { getPermalink, getBlogPermalink, getAsset } from './utils/permalinks';
|
||||
|
||||
export const headerData = {
|
||||
links: [
|
||||
{
|
||||
text: 'Home',
|
||||
links: [
|
||||
{
|
||||
text: 'About',
|
||||
href: getPermalink('/#about'),
|
||||
},
|
||||
{
|
||||
text: 'Features',
|
||||
href: getPermalink('/#features'),
|
||||
},
|
||||
{
|
||||
text: 'Why Zanzibar',
|
||||
href: getPermalink('#zanzibar'),
|
||||
},
|
||||
{
|
||||
text: 'Benefits',
|
||||
href: getPermalink('#benefits'),
|
||||
},
|
||||
{
|
||||
text: 'Presale',
|
||||
href: getPermalink('#presale'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'FAQ',
|
||||
href: '#faq',
|
||||
},
|
||||
{
|
||||
text: 'Blog',
|
||||
href: '/blog',
|
||||
},
|
||||
{
|
||||
text: 'Docs',
|
||||
href: 'https://ourworldfreezone.github.io/info_freezone/intro/intro_readme.html',
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
actions: [{ text: 'Contact', href: '/contact', target: '_blank' }],
|
||||
};
|
||||
|
||||
export const footerData = {
|
||||
links: [
|
||||
{
|
||||
title: 'OurWorld',
|
||||
links: [
|
||||
{ text: 'OurWorld', href: 'https://ourworld.tf' },
|
||||
{ text: 'ODFZ Demo', href: 'https://ourworld.vindo.ai/slider/95' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Partners',
|
||||
links: [
|
||||
{ text: 'ThreeFold', href: 'https://threefold.io' },
|
||||
{ text: 'Govt of Zanzibar', href: 'https://egaz.go.tz/en/' },
|
||||
{ text: 'Incubaid', href: 'https://incubaid.com/' },
|
||||
{ text: 'Sikana', href: 'https://sikana.tv/' },
|
||||
{ text: 'VVerse', href: 'https://vverse.co/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
links: [
|
||||
{ text: 'Docs', href: 'https://ourworldfreezone.github.io/info_freezone/intro/intro_readme.html' },
|
||||
{ text: 'Community Forum', href: 'https://forum.threefold.io' },
|
||||
{ text: 'Telegram', href: 'https://t.me/threefoldnews' },
|
||||
{ text: 'Support', href: 'https://threefoldfaq.crisp.help/en/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ text: 'About', href: '#about' },
|
||||
{ text: 'Blog', href: '#blog' },
|
||||
{ text: 'FAQ', href: '#faq' },
|
||||
{ text: 'Contact', href: '/contact' },
|
||||
],
|
||||
},
|
||||
],
|
||||
secondaryLinks: [
|
||||
{ text: 'Terms', href:'https://library.threefold.me/info/legal/#/legal__terms_conditions_websites', target: '_blank', },
|
||||
{ text: 'Privacy Policy', href: 'https://library.threefold.me/info/legal/#/legal__privacypolicy', target: '_blank', },
|
||||
],
|
||||
socialLinks: [
|
||||
{ ariaLabel: 'Telegram', icon: 'tabler:brand-telegram', href: 'https://t.me/threefoldnews' },
|
||||
{ ariaLabel: 'Github', icon: 'tabler:brand-github', href: 'https://github.com/ourworldventures/' },
|
||||
{ ariaLabel: 'RSS', icon: 'tabler:rss', href: getAsset('/rss.xml') },
|
||||
{ ariaLabel: 'Docs', icon: 'tabler:book', href: 'https://ourworldfreezone.github.io/info_freezone/intro/intro_readme.html' },
|
||||
],
|
||||
footNote: `
|
||||
<img class="w-5 h-5 md:w-6 md:h-6 md:-mt-0.5 bg-cover mr-1.5 rtl:mr-0 rtl:ml-1.5 float-left rtl:float-right rounded-sm" src="/src/assets/images/logo.svg" alt="onWidget logo" loading="lazy"></img>
|
||||
Made by <a class="text-blue-600 underline dark:text-muted" href="https://ourworld.tf/"> OurWorld Holdings</a> · All rights reserved.
|
||||
`,
|
||||
};
|