This commit is contained in:
Gary Kwok
2024-06-17 18:05:05 +08:00
commit 56f6d03385
105 changed files with 4350 additions and 0 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
; https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# build output
dist/
.output/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
yarn.lock
package-lock.json
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# ignore .astro directory
.astro

4
.markdownlint.json Normal file
View File

@@ -0,0 +1,4 @@
{
"MD033": false,
"MD013": false
}

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": ["*.astro"],
"options": {
"parser": "astro"
}
}
]
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["astro-build.astro-vscode"]
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.mdx": "markdown"
}
}

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:lts AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 8080

45
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,45 @@
pipeline {
environment {
GIT_URL='http://192.168.11.55:4000/GiteaTeam/astro.git'
GIT_CREDENTIAL_ID = '82ae2d4d36ce107d337ec03c24c6fd9e39fef6ea'
GIT_BRANCH = 'main'
REGISTRY = 'pvgharbor.duckdns.org'
REGISTRY_CREDENTIAL_ID = 'astro'
DOCKER_USERNAME = 'ansible'
DOCKER_PASSWORD = 'P@ssw0rd'
project = "astro"
app_name = "astro"
label = "v1.4"
images_repository = "${REGISTRY}/${project}/${app_name}"
image_name = "${REGISTRY}/${project}/${app_name}:${label}"
secret_name = "harbor"
docker_registry_auth = "98195b64-dc71-42d6-899b-46dd4b566242"
}
agent any
stages {
stage('SCM Checkout') {
steps {
git branch: "${GIT_BRANCH}", credentialsId: "${GIT_CREDENTIAL_ID}", url: "${GIT_URL}"
}
}
stage('docker build & push') {
steps {
withCredentials([usernamePassword(credentialsId: "${docker_registry_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
sh ('docker build -t ${image_name} .')
sh ('echo "$DOCKER_PASSWORD" | docker login $images_repository -u "$DOCKER_USERNAME" --password-stdin')
sh ('docker push $image_name')
}
}
}
}
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2023 - Present, Themefisher
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.

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
<h1 align=center>Bookworm Light Astro</h1>
<p align=center>Bookworm Light is a feature-rich, minimal, highly customizable, easy-to-use free Astro blog theme.</p>
<h2 align="center"> <a target="_blank" href="https://bookworm-light-astro.vercel.app/" rel="nofollow">👀Demo</a> | <a target="_blank" href="https://pagespeed.web.dev/report?url=https%3A%2F%2Fbookworm-light-astro.vercel.app%2F&form_factor=desktop">Page Speed (100%)🚀</a>
</h2>
<p align=center>
<a href="https://github.com/withastro/astro/releases/tag/astro%404.3.2" alt="Contributors">
<img src="https://img.shields.io/static/v1?label=ASTRO&message=4.3&color=000&logo=astro" />
</a>
<a href="https://github.com/themefisher/bookworm-light-astro/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/themefisher/bookworm-light-astro" alt="license"></a>
<img src="https://img.shields.io/github/languages/code-size/themefisher/bookworm-light-astro" alt="code size">
<a href="https://github.com/themefisher/bookworm-light-astro/graphs/contributors">
<img src="https://img.shields.io/github/contributors/themefisher/bigspring-light-astro" alt="contributors"></a>
</p>
![bookworm-light](https://demo.gethugothemes.com/thumbnails/bookworm-light.png)
Bookworm Light is a minimal multi-author free Astro blog theme which is perfect for any kind of blog website. Whether you're interested in food, beauty, travel, photography, lifestyle, fitness, health, or other topics, this theme is a great fit. The theme is super fast and SEO friendly which makes it easier for your content to be discovered by search engines.
## 🔑Key Features
- 🎨 Highly Customizable (Color, Font, Menu, Social Links, SEO Meta Tags, etc.)
- 👥 Multi-Author Support
- 📚 Authors Page
- 👤 Author Single Page
- 🔍 Search Functionality with FuseJS
- 🏷️ Tags and Categories Support
- 📲 Post Social Share Option
- 🔗 Similar Post Suggestions
- ⚡ Fast by Default (95+ Google PageSpeed Score)
- ⚙️ Netlify Settings Pre-configured
- 📬 Contact Form Support
- 🌅 Support OG Image
- ✍️ Write and Update Content in Markdown / MDX
- 📚 MDX Components Auto Import
- 📝 Includes Draft Pages and Posts
- 🚀 Built with Tailwind CSS Framework
- 📱 Fully Responsive on Desktops, Tablets, and Smartphones
- 🔍 SEO Friendly
<!-- installation -->
## 🔧Installation
After downloading the template, you have some prerequisites to install. Then you can run it on your localhost. You can view the package.json file to see which scripts are included.
### ⚙Install prerequisites (once for a machine)
- **Node Installation:** [Install node js](https://nodejs.org/en/download/) [Recommended LTS version]
### 🖥Local setup
After successfully installing those dependencies, open this template with any IDE [[VS Code](https://code.visualstudio.com/) recommended], and then open the internal terminal of IDM [vs code shortcut <code>ctrl/cmd+\`</code>]
- Install dependencies
```
npm install
```
- Run locally
```
npm run dev
```
After that, it will open up a preview of the template in your default browser, watch for changes to source files, and live-reload the browser when changes are saved.
## 🔨Production Build
After finishing all the customization, you can create a production build by running this command.
```
npm run build
```
<!-- reporting issue -->
## 🐞Reporting Issues
We use GitHub Issues as the official bug tracker for this Template. Please Search [existing issues](https://github.com/themefisher/bookworm-light-astro/issues). Its possible someone has already reported the same problem.
If your problem or idea has not been addressed yet, feel free to [open a new issue](https://github.com/themefisher/bookworm-light-astro/issues).
<!-- licence -->
## 📄License
Copyright (c) 2023 - Present, Designed & Developed by [Themefisher](https://themefisher.com)
**Code License:** Released under the [MIT](https://github.com/themefisher/bookworm-light-astro/blob/main/LICENSE) license.
**Image license:** The images are only for demonstration purposes. They have their license, we don't have permission to share those images.
## 👨💻Need Custom Development Services?
Besides developing beautifully designed and blazing-fast themes, we help businesses create fast, performance-focused, scalable & secure websites based on NextJs, Hugo, Astro, etc.
If you need a custom theme, theme customization, or complete website development services from scratch you can [Hire Us](https://themefisher.com/contact).

56
astro.config.mjs Normal file
View File

@@ -0,0 +1,56 @@
import mdx from "@astrojs/mdx";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind";
import AutoImport from "astro-auto-import";
import { defineConfig, squooshImageService } from "astro/config";
import remarkCollapse from "remark-collapse";
import remarkToc from "remark-toc";
import config from "./src/config/config.json";
// https://astro.build/config
export default defineConfig({
site: config.site.base_url ? config.site.base_url : "http://examplesite.com",
base: config.site.base_path ? config.site.base_path : "/",
trailingSlash: config.site.trailing_slash ? "always" : "never",
image: {
service: squooshImageService(),
},
integrations: [
react(),
sitemap(),
tailwind({
config: {
applyBaseStyles: false,
},
}),
AutoImport({
imports: [
"@/shortcodes/Button",
"@/shortcodes/Accordion",
"@/shortcodes/Notice",
"@/shortcodes/Video",
"@/shortcodes/Youtube",
"@/shortcodes/Tabs",
"@/shortcodes/Tab",
],
}),
mdx(),
],
markdown: {
remarkPlugins: [
remarkToc,
[
remarkCollapse,
{
test: "Table of contents",
},
],
],
shikiConfig: {
theme: "one-dark-pro",
wrap: true,
},
extendDefaultPlugins: true,
},
});

12
netlify.toml Normal file
View File

@@ -0,0 +1,12 @@
[build]
publish = "dist"
command = "yarn build"
[[headers]]
for = "/*" # This defines which paths this specific [[headers]] block will cover.
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "same-origin"
Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"

31
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,31 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html index.htm;
include /etc/nginx/mime.types;
gzip on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
internal;
}
location / {
try_files $uri $uri/index.html =404;
}
}
}

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "bookworm-light-astro",
"version": "2.0.0",
"license": "MIT",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"format": "prettier -w ."
},
"dependencies": {
"@astrojs/mdx": "^2.1.1",
"@astrojs/rss": "^4.0.4",
"@astrojs/react": "^3.0.9",
"@astrojs/sitemap": "^3.0.5",
"@astrojs/tailwind": "^5.1.0",
"astro": "^4.3.2",
"astro-auto-import": "^0.4.2",
"astro-font": "^0.0.77",
"date-fns": "^3.3.1",
"fuse.js": "^7.0.0",
"gray-matter": "^4.0.3",
"marked": "^12.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-lite-youtube-embed": "^2.4.0",
"remark-collapse": "^0.1.2",
"remark-toc": "^9.0.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/github-slugger": "^1.3.0",
"@types/marked": "^5.0.2",
"@types/react": "^18.2.52",
"postcss": "^8.4.33",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0",
"prettier-plugin-tailwindcss": "^0.5.11",
"sass": "^1.70.0",
"sharp": "0.33.1",
"tailwind-bootstrap-grid": "^5.1.0",
"tailwindcss": "^3.4.1",
"typescript": "5.3.3"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

88
public/.htaccess Normal file
View File

@@ -0,0 +1,88 @@
##### Optimize default expiration time - BEGIN
<IfModule mod_expires.c>
## Enable expiration control
ExpiresActive On
## CSS and JS expiration: 1 week after request
ExpiresByType text/css "now plus 1 week"
ExpiresByType application/javascript "now plus 1 week"
ExpiresByType application/x-javascript "now plus 1 week"
## Image files expiration: 1 month after request
ExpiresByType image/bmp "now plus 1 month"
ExpiresByType image/gif "now plus 1 month"
ExpiresByType image/jpeg "now plus 1 month"
ExpiresByType image/webp "now plus 1 month"
ExpiresByType image/jp2 "now plus 1 month"
ExpiresByType image/pipeg "now plus 1 month"
ExpiresByType image/png "now plus 1 month"
ExpiresByType image/svg+xml "now plus 1 month"
ExpiresByType image/tiff "now plus 1 month"
ExpiresByType image/x-icon "now plus 1 month"
ExpiresByType image/ico "now plus 1 month"
ExpiresByType image/icon "now plus 1 month"
ExpiresByType text/ico "now plus 1 month"
ExpiresByType application/ico "now plus 1 month"
ExpiresByType image/vnd.wap.wbmp "now plus 1 month"
## Font files expiration: 1 month after request
ExpiresByType application/x-font-ttf "now plus 1 month"
ExpiresByType application/x-font-opentype "now plus 1 month"
ExpiresByType application/x-font-woff "now plus 1 month"
ExpiresByType font/woff2 "now plus 1 month"
ExpiresByType image/svg+xml "now plus 1 month"
## Audio files expiration: 1 month after request
ExpiresByType audio/ogg "now plus 1 month"
ExpiresByType application/ogg "now plus 1 month"
ExpiresByType audio/basic "now plus 1 month"
ExpiresByType audio/mid "now plus 1 month"
ExpiresByType audio/midi "now plus 1 month"
ExpiresByType audio/mpeg "now plus 1 month"
ExpiresByType audio/mp3 "now plus 1 month"
ExpiresByType audio/x-aiff "now plus 1 month"
ExpiresByType audio/x-mpegurl "now plus 1 month"
ExpiresByType audio/x-pn-realaudio "now plus 1 month"
ExpiresByType audio/x-wav "now plus 1 month"
## Movie files expiration: 1 month after request
ExpiresByType application/x-shockwave-flash "now plus 1 month"
ExpiresByType x-world/x-vrml "now plus 1 month"
ExpiresByType video/x-msvideo "now plus 1 month"
ExpiresByType video/mpeg "now plus 1 month"
ExpiresByType video/mp4 "now plus 1 month"
ExpiresByType video/quicktime "now plus 1 month"
ExpiresByType video/x-la-asf "now plus 1 month"
ExpiresByType video/x-ms-asf "now plus 1 month"
</IfModule>
##### Optimize default expiration time - END
##### 1 Month for most static resources
<filesMatch ".(css|jpg|jpeg|png|webp|gif|js|ico|woff|woff2|eot|ttf)$">
Header set Cache-Control "max-age=2592000, public"
</filesMatch>
##### Enable gzip compression for resources
<ifModule mod_gzip.c>
mod_gzip_on Yes
mod_gzip_dechunk Yes
mod_gzip_item_include file .(html?|txt|css|js|php)$
mod_gzip_item_include handler ^cgi-script$
mod_gzip_item_include mime ^text/.*
mod_gzip_item_include mime ^application/x-javascript.*
mod_gzip_item_exclude mime ^image/.*
mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
</ifModule>
##### Or, compress certain file types by extension:
<FilesMatch ".(html|css|jpg|jpeg|webp|png|gif|js|ico)">
SetOutputFilter DEFLATE
</FilesMatch>
##### Set Header Vary: Accept-Encoding
<IfModule mod_headers.c>
<FilesMatch ".(js|css|xml|gz|html)$">
Header append Vary: Accept-Encoding
</FilesMatch>
</IfModule>

BIN
public/images/author.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
public/images/posts/01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/images/posts/02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/images/posts/03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/images/posts/04.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/images/posts/05.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/images/posts/06.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/images/posts/07.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Disallow: /api/*

35
src/config/config.json Normal file
View File

@@ -0,0 +1,35 @@
{
"site": {
"title": "RT",
"base_url": "https://blog.currytech.online",
"base_path": "/",
"trailing_slash": false,
"favicon": "/images/favicon.png",
"logo": "/images/logo.png",
"logo_width": "200",
"logo_height": "34",
"logo_text": "RT"
},
"settings": {
"pagination": 5,
"summary_length": 150
},
"metadata": {
"meta_author": "CurryTech",
"meta_image": "/images/og-image.png",
"meta_description": "CurryTech - A Technical Blog"
},
"params": {
"contact_form_action": "#",
"copyright": "Copyright © 2023 - Build by Astro on K3S "
},
"contactinfo": {
"address": "",
"email": "info@currytech.oline",
"phone": ""
}
}

45
src/config/menu.json Normal file
View File

@@ -0,0 +1,45 @@
{
"main": [
{
"name": "Home",
"url": "/"
},
{
"name": "Tags",
"url": "/tags"
},
{
"name": "Categories",
"url": "/categories"
},
{
"name": "Pages",
"url": "",
"hasChildren": true,
"children": [
{
"name": "Contact",
"url": "/contact"
},
{
"name": "About",
"url": "/about"
}
]
}
],
"footer": [
{
"name": "About",
"url": "/about"
},
{
"name": "Contact",
"url": "/contact"
},
{
"name": "Privacy Policy",
"url": "/privacy-policy"
}
]
}

30
src/config/social.json Normal file
View File

@@ -0,0 +1,30 @@
{
"facebook": "https://facebook.com/",
"twitter": "https://twitter.com/",
"instagram": "https://instagram.com/",
"youtube": "https://youtube.com/",
"linkedin": "https://linkedin.com/",
"github": "",
"gitlab": "",
"medium": "",
"codepen": "",
"bitbucket": "",
"dribbble": "",
"behance": "",
"pinterest": "",
"soundcloud": "",
"tumblr": "",
"reddit": "",
"vk": "",
"whatsapp": "",
"snapchat": "",
"vimeo": "",
"tiktok": "",
"foursquare": "",
"rss": "",
"email": "",
"phone": "",
"address": "",
"skype": "",
"website": ""
}

30
src/config/theme.json Normal file
View File

@@ -0,0 +1,30 @@
{
"colors": {
"default": {
"theme_color": {
"primary": "#01AD9F",
"body": "#fff",
"border": "#D5D5D5",
"theme_light": "#FAFAFA",
"theme_dark": "#152035"
},
"text_color": {
"default": "#747577",
"dark": "#152035",
"light": "#a1a5ae"
}
}
},
"fonts": {
"font_family": {
"primary": "Mulish:wght@400;600;700",
"primary_type": "sans-serif",
"secondary": "",
"secondary_type": ""
},
"font_size": {
"base": "16",
"scale": "1.250"
}
}
}

View File

@@ -0,0 +1,23 @@
---
title: "Im John Doe, A content writer based in LDN, Currently at Bookworm"
meta_title: "About"
image: "/images/author.png"
draft: false
what_i_do:
title: "What I Do"
items:
- title: "Content Writing"
description: "Purus eget ipsum elementum venenatis, quis rutrum mi semper nonpurus eget ipsum elementum venenatis."
- title: "Photography"
description: "Aenean maximus urna magna elementum, quis rutrum mi semper non purus eget ipsum venenatis."
- title: "Web Research"
description: "Aenean maximus urna magna elementum venenatis, quis semper non purus eget ipsum venenatis."
---
A content writer with over 12 years experience working across brand identity, publishing and digital products. Maecenas sit amet purus eget ipsum elementum venenatis. Aenean maximus urna magna elementum venenatis quis non purus.
Purus eget ipsum elementum venenatis. Aenean maximus urna magna elementum venenatis, quis rutrum mi semper non purus eget ipsum elementum venenatis, aenean maximus urna magna elementum.

View File

@@ -0,0 +1,3 @@
---
title: "Authors"
---

View File

@@ -0,0 +1,11 @@
---
title: John Doe
image: /images/authors/john-doe.jpg
description: this is meta description
social:
facebook: https://www.facebook.com/
twitter: https://www.twitter.com/
instagram: https://www.instagram.com/
---
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua.

View File

@@ -0,0 +1,11 @@
---
title: Mark Dinn
image: /images/authors/mark-dinn.jpg
description: this is meta description
social:
facebook: https://www.facebook.com/
twitter: https://www.twitter.com/
instagram: https://www.instagram.com/
---
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua.

56
src/content/config.ts Normal file
View File

@@ -0,0 +1,56 @@
import { defineCollection, z } from "astro:content";
// Post collection schema
const postsCollection = defineCollection({
schema: z.object({
id: z.string().optional(),
title: z.string(),
meta_title: z.string().optional(),
description: z.string().optional(),
date: z.date().optional(),
image: z.string().optional(),
authors: z.array(z.string()).default(["admin"]),
categories: z.array(z.string()).default(["others"]),
tags: z.array(z.string()).default(["others"]),
draft: z.boolean().optional(),
}),
});
// Author collection schema
const authorsCollection = defineCollection({
schema: z.object({
id: z.string().optional(),
title: z.string(),
meta_title: z.string().optional(),
image: z.string().optional(),
description: z.string().optional(),
social: z
.object({
facebook: z.string().optional(),
twitter: z.string().optional(),
instagram: z.string().optional(),
})
.optional(),
draft: z.boolean().optional(),
}),
});
// Pages collection schema
const pagesCollection = defineCollection({
schema: z.object({
id: z.string().optional(),
title: z.string(),
meta_title: z.string().optional(),
description: z.string().optional(),
image: z.string().optional(),
layout: z.string().optional(),
draft: z.boolean().optional(),
}),
});
// Export collections
export const collections = {
posts: postsCollection,
pages: pagesCollection,
authors: authorsCollection,
};

5
src/content/pages/404.md Normal file
View File

@@ -0,0 +1,5 @@
---
title: "Error 404"
---
## Page Not Found

View File

@@ -0,0 +1,4 @@
---
title: "Contact"
draft: false
---

View File

@@ -0,0 +1,255 @@
---
title: "Elements"
meta_title: ""
description: "this is meta description"
draft: false
---
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
---
### Paragraph
Did you come here for something in particular or just general Riker-bashing? And blowing into maximum warp speed, you appeared for an instant to be in two places at once. We have a saboteur aboard. We know youre dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf. Could someone survive inside a transporter buffer for 75 years? Fate. It protects fools, little children, and ships.
Did you come here for something in particular or just general Riker-bashing? And blowing into maximum warp speed, you appeared for an instant to be in two places at once. We have a saboteur aboard. We know youre dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf. Could someone survive inside a transporter buffer for 75 years? Fate. It protects fools, little children, and ships.
---
### Emphasis
1. Did you come here for something in **particular** or just general
2. Did you come here for something in <ins>particular</ins>
3. _Did you come here_
4. Did you come here for **something** in particular
5. Did you come here for something in particular
6. Did you come here for something in particular
7. URLs and URLs in angle brackets will automatically get turned into links. [http://www.example.com](http://www.example.com) or
8. [http://www.example.com](http://www.example.com) and sometimes example.com (but not on Github, for example).
---
### Link
[I'm an inline-style link](https://www.google.com)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][arbitrary case-insensitive reference text]
[I'm a relative reference to a repository file](../blob/master/LICENSE)
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.themefisher.com
[1]: https://gethugothemes.com
[link text itself]: https://www.getjekyllthemes.com
---
### Ordered List
1. List item
2. List item
3. List item
4. List item
5. List item
---
### Unordered List
- List item
- List item
- List item
- List item
- List item
---
### Code and Syntax Highlighting
#### HTML
```html
<ul>
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="about/">About</a>
</li>
</ul>
```
---
#### CSS
```css
img {
vertical-align: middle;
border: 0;
max-width: 100%;
height: auto;
}
```
---
#### JavaScript
```javascript
window.addEventListener("load", (e) => {
document.querySelector(".preloader").style.display = "none";
});
```
---
### Button
<Button label="Button" link="#" style="solid" />
---
### Quote
> Did you come here for something in particular or just general Riker-bashing? And blowing into maximum warp speed, you appeared for an instant to be in two places at once.
---
### Notice
<Notice type="note">This is a simple note.</Notice>
<Notice type="tip">This is a simple note.</Notice>
<Notice type="info">This is a simple note.</Notice>
<Notice type="warning">This is a simple note.</Notice>
---
### Tab
<Tabs client:load>
<Tab name="Tab 1">
#### Did you come here for something in particular?
Did you come here for something in particular or just general Riker-bashing? And blowing into maximum warp speed, you appeared for an instant to be in two places at once. We have a saboteur aboard. We know youre dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf.
</Tab>
<Tab name="Tab 2">
#### I wanna talk about the assassination attempt
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</Tab>
<Tab name="Tab 3">
#### We know youre dealing in stolen ore
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo
</Tab>
</Tabs>
---
### Table
| # | First | Last | Handle |
| :-- | :----------: | :----------: | -----------: |
| 1 | Row:1 Cell:1 | Row:1 Cell:2 | Row:1 Cell:3 |
| 2 | Row:2 Cell:1 | Row:2 Cell:2 | Row:2 Cell:3 |
| 3 | Row:3 Cell:1 | Row:3 Cell:2 | Row:3 Cell:3 |
---
### Accordion
<Accordion client:load title="Why should you need to do this?">
- This is a thing.
- This is a thing.
- This is a thing.
- This is a thing.
- This is a thing.
</Accordion>
<Accordion client:load title="How can I adjust Horizontal centering">
- This is a thing.
- This is a thing.
- This is a thing.
- This is a thing.
- This is a thing.
</Accordion>
<Accordion client:load title="Should you use Negative margin?">
- This is a thing.
- This is a thing.
- This is a thing.
- This is a thing.
- This is a thing.
</Accordion>
---
### Image
![image](/images/image-placeholder.png)
---
### Youtube video
<Youtube client:load id="ZEe-IFezQok" title="Youtube Video Test Title" />
---
### Custom video
<Video
width="100%"
src="https://joy1.videvo.net/videvo_files/video/free/video0467/large_watermarked/_import_61516692993d77.04238324_preview.mp4"
/>

View File

@@ -0,0 +1,29 @@
---
title: "Privacy"
description: "this is meta description"
draft: false
---
#### Responsibility of Contributors
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat quisque aliquam sagittis. Sem turpis sed viverra massa gravida pharetra. Non dui dolor potenti eu dignissim fusce. Ultrices amet, in curabitur a arcu a lectus morbi id. Iaculis erat sagittis in tortor cursus. Molestie urna eu tortor, erat scelerisque eget. Nunc hendrerit sed interdum lacus. Lorem quis viverra sed
pretium, aliquam sit. Praesent elementum magna amet, tincidunt eros, nibh in leo. Malesuada purus, lacus, at aliquam suspendisse tempus. Quis tempus amet, velit nascetur sollicitudin. At sollicitudin eget amet in. Eu velit nascetur sollicitudin erhdfvssfvrgss eget viverra nec elementum. Lacus, facilisis tristique lectus in.
#### Gathering of Personal Information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat quisque aliquam sagittis. Sem turpis sed viverra massa gravida pharetra. Non dui dolor potenti eu dignissim fusce. Ultrices amet, in curabitur a arcu a lectus morbi id. Iaculis erat sagittis in tortor cursus. Molestie urna eu tortor, erat scelerisque eget. Nunc hendrerit sed interdum lacus. Lorem quis viverra sed
#### Protection of Personal- Information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat quisque aliquam sagittis. Sem turpis sed viverra massa gravida pharetra. Non dui dolor potenti eu dignissim fusce. Ultrices amet, in curabitur a arcu a lectus morbi id. Iaculis erat sagittis in tortor cursus.
Molestie urna eu tortor, erat scelerisque eget. Nunc hendrerit sed interdum lacus. Lorem quis viverra sed
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat
#### Privacy Policy Changes
1. Sll the Themefisher items are designed to be with the latest , We check all
2. comments that threaten or harm the reputation of any person or organization
3. personal information including, but limited to, email addresses, telephone numbers
4. Any Update come in The technology Customer will get automatic Notification.

View File

@@ -0,0 +1,3 @@
---
title: Blog
---

View File

@@ -0,0 +1,22 @@
---
title: "How to make toys from old Olarpaper"
description: "meta description"
date: 2022-04-04T05:00:00Z
image: "/images/posts/01.jpg"
categories: ["art"]
authors: ["Mark Dinn"]
tags: ["diy", "toy"]
draft: false
---
Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
## Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

View File

@@ -0,0 +1,22 @@
---
title: Why a balloon is a flexible bag
description: "meta description"
date: 2022-04-04T05:00:00Z
image: "/images/posts/02.jpg"
categories: ["development"]
authors: ["John Doe"]
tags: ["diy", "toy"]
draft: false
---
Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
## Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

View File

@@ -0,0 +1,22 @@
---
title: What you need to know about Photography
description: "meta description"
date: 2022-04-02T06:00:00+00:00
image: "/images/posts/03.jpg"
categories: ["art"]
authors: ["Mark Dinn"]
tags: ["diy", "toy"]
draft: false
---
Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
## Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

View File

@@ -0,0 +1,22 @@
---
title: "Why a balloon is a flexible bag that can be inflated with a gas"
description: "meta description"
image: "/images/posts/04.jpg"
date: 2021-02-02T16:56:47+06:00
draft: false
authors: ["Mark Dinn"]
tags: ["Balloon", "Gas"]
categories: ["Accessories"]
---
A balloon is a flexible bag that can be inflated with a gas, such as helium, hydrogen, nitrous oxide, oxygen, and air. For special tasks, balloons can be filled with smoke, liquid water, granular media, or light sources.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
### Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

View File

@@ -0,0 +1,22 @@
---
title: "Banana is the best meal packages in the UK spark outrage online"
description: "meta description"
image: "/images/posts/05.jpg"
date: 2021-01-25T16:56:47+06:00
draft: false
authors: ["John Doe"]
tags: ["Food", "Gold"]
categories: ["Food"]
---
A banana is an elongated, edible fruit botanically a berry produced by several kinds of large herbaceous flowering plants in the genus Musa. In some countries, bananas used for cooking may be called "plantains", distinguishing them from dessert bananas.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
### Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

View File

@@ -0,0 +1,22 @@
---
title: "Become a morning person with the help of alarm clock"
description: "meta description"
image: "/images/posts/06.jpg"
date: 2021-02-03T16:56:47+06:00
draft: false
authors: ["Mark Dinn"]
tags: ["Alarm", "Clock"]
categories: ["LifeStyle"]
---
Almost every day for the past nine or so months has felt like March 13, and that can sometimes make it difficult to want to wake up for the day ahead of you.
To make a morning person out of you, the wake-up light simulates the sunrise to gradually ease you awake. This allows you to wake up more naturally rather than being jolted awake by the default iPhone alarm sound, which honestly triggers my fight or flight response.
### Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

View File

@@ -0,0 +1,21 @@
---
title: "How to make toys from old paper"
description: "meta description"
date: 2022-06-13T05:00:00Z
image: "/images/posts/01.jpg"
categories: ["art"]
authors: ["Mark Dinn"]
tags: ["diy", "toy"]
draft: false
---
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
## Creative Design
Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

2
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="@astrojs/image/client" />

View File

@@ -0,0 +1,35 @@
---
import { Image } from "astro:assets";
import Social from "@/components/Social.astro";
import { markdownify } from "@/lib/utils/textConverter";
const { author } = Astro.props;
const { title, image, social } = author.data;
const { Content } = await author.render();
---
<section class="section">
<div class="container">
<div class="mb-4 text-center md:px-24">
{
image && (
<div class="mb-8">
<Image
src={image}
class="mx-auto rounded-lg"
height={150}
width={150}
alt={title}
/>
</div>
)
}
<h1 set:html={markdownify(title)} class="page-heading h2 mb-8" />
<Social source={social} className="social-icons-simple" />
<div class="content">
<Content />
</div>
</div>
</div>
</section>

42
src/layouts/Authors.astro Normal file
View File

@@ -0,0 +1,42 @@
---
import { markdownify } from "@/lib/utils/textConverter";
import { Image } from "astro:assets";
import { BsArrowRightCircle } from "react-icons/bs";
import type { TAuthor } from "@/types";
const { authors } = Astro.props;
---
<div class="row justify-center">
{
authors.map((author: TAuthor) => (
<div class="col-12 mb-8 sm:col-6 md:col-4">
{author.data.image && (
<div class="mb-4">
<Image
src={author.data.image}
alt={author.data.title}
height={150}
width={150}
class="mx-auto rounded-lg"
/>
</div>
)}
<h3 class="h4 mt-8 mb-3">
<a
href={`/authors/${author.slug}`}
class="block hover:text-primary transition duration-200"
>
{author.data.title}
</a>
</h3>
<p class="mb-3" set:text={markdownify(author.body.slice(0, 100))} />
<a
href={`/authors/${author.slug}`}
class="inline-flex items-center text-primary transition duration-200 hover:opacity-70"
>
Read More <BsArrowRightCircle className="inline ml-2" />
</a>
</div>
))
}
</div>

180
src/layouts/Base.astro Normal file
View File

@@ -0,0 +1,180 @@
---
import TwSizeIndicator from "@/components/TwSizeIndicator.astro";
import config from "@/config/config.json";
import theme from "@/config/theme.json";
import { plainify } from "@/lib/utils/textConverter";
import Footer from "@/partials/Footer.astro";
import Header from "@/partials/Header.astro";
import "@/styles/main.scss";
import { AstroFont } from "astro-font";
import { ViewTransitions } from "astro:transitions";
// font families
const pf = theme.fonts.font_family.primary;
const sf = theme.fonts.font_family.secondary;
let fontPrimary, fontSecondary;
if (theme.fonts.font_family.primary) {
fontPrimary = theme.fonts.font_family.primary
.replace(/\+/g, " ")
.replace(/:[ital,]*[ital@]*[wght@]*[0-9,;]+/gi, "");
}
if (theme.fonts.font_family.secondary) {
fontSecondary = theme.fonts.font_family.secondary
.replace(/\+/g, " ")
.replace(/:[ital,]*[ital@]*[wght@]*[0-9,;]+/gi, "");
}
// types for frontmatters
export interface Props {
title?: string;
meta_title?: string;
description?: string;
image?: string;
noindex?: boolean;
canonical?: string;
}
// distructure frontmatters
const { title, meta_title, description, image, noindex, canonical } =
Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<!-- favicon -->
<link rel="shortcut icon" href={config.site.favicon} />
<!-- theme meta -->
<meta name="theme-name" content="bookworm-light-astro" />
<meta name="msapplication-TileColor" content="#000000" />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="#fff"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#000"
/>
<meta name="generator" content={Astro.generator} />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- google font css -->
<AstroFont
config={[
{
src: [],
preload: false,
display: "swap",
name: fontPrimary!,
fallback: "sans-serif",
cssVariable: "font-primary",
googleFontsURL: `https://fonts.googleapis.com/css2?family=${pf}&display=swap`,
},
{
src: [],
preload: false,
display: "swap",
name: fontSecondary!,
fallback: "sans-serif",
cssVariable: "font-secondary",
googleFontsURL: `https://fonts.googleapis.com/css2?family=${sf}&display=swap`,
},
]}
/>
<!-- responsive meta -->
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=5"
/>
<!-- title -->
<title>
{plainify(meta_title ? meta_title : title ? title : config.site.title)}
</title>
<!-- canonical url -->
{canonical && <link rel="canonical" href={canonical} item-prop="url" />}
<!-- noindex robots -->
{noindex && <meta name="robots" content="noindex,nofollow" />}
<!-- meta-description -->
<meta
name="description"
content={plainify(
description ? description : config.metadata.meta_description,
)}
/>
<ViewTransitions />
<!-- author from config.json -->
<meta name="author" content={config.metadata.meta_author} />
<!-- og-title -->
<meta
property="og:title"
content={plainify(
meta_title ? meta_title : title ? title : config.site.title,
)}
/>
<!-- og-description -->
<meta
property="og:description"
content={plainify(
description ? description : config.metadata.meta_description,
)}
/>
<meta property="og:type" content="website" />
<meta
property="og:url"
content={`${config.site.base_url}/${Astro.url.pathname.replace("/", "")}`}
/>
<!-- twitter-title -->
<meta
name="twitter:title"
content={plainify(
meta_title ? meta_title : title ? title : config.site.title,
)}
/>
<!-- twitter-description -->
<meta
name="twitter:description"
content={plainify(
description ? description : config.metadata.meta_description,
)}
/>
<!-- og-image -->
<meta
property="og:image"
content={`${config.site.base_url}${
image ? image : config.metadata.meta_image
}`}
/>
<!-- twitter-image -->
<meta
name="twitter:image"
content={`${config.site.base_url}${
image ? image : config.metadata.meta_image
}`}
/>
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<TwSizeIndicator />
<Header />
<main id="main-content" class="min-h-[60vh]">
<slot />
</main>
<Footer />
</body>
</html>

16
src/layouts/Default.astro Normal file
View File

@@ -0,0 +1,16 @@
---
import { markdownify } from "@/lib/utils/textConverter";
const { data } = Astro.props;
const { title } = data.data;
const { Content } = await data.render();
---
<section class="section">
<div class="container">
<h1 set:html={markdownify(title)} class="h2 page-heading" />
<div class="content">
<Content />
</div>
</div>
</section>

View File

@@ -0,0 +1,139 @@
---
import { Image } from "astro:assets";
import Share from "@/components/Share.astro";
import SimilarPosts from "@/components/SimilarPosts.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import dateFormat from "@/lib/utils/dateFormat";
import similerItems from "@/lib/utils/similarItems";
import { humanize, markdownify, slugify } from "@/lib/utils/textConverter";
import { BiCalendarEdit, BiCategoryAlt } from "react-icons/bi";
const allAuthors = await getSinglePage("authors");
const posts = await getSinglePage("posts");
const { post } = Astro.props;
const similarPosts = similerItems(post, posts, post.slug);
const { Content } = await post.render();
const { title, description, authors, categories, image, date, tags } =
post.data;
---
<section class="section">
<div class="container">
<article class="row justify-center">
<div class="md:col-10 text-center">
<h1 set:html={markdownify(title)} class="h2" />
<ul class="mt-4 flex flex-wrap items-center justify-center text-text">
<li class="mx-3">
{
allAuthors
.filter((author) =>
authors
.map((author: string) => slugify(author))
.includes(slugify(author.data.title)),
)
.map((author, i) => (
<a
href={`/authors/${slugify(author.slug)}`}
class="flex items-center hover:text-primary font-medium"
>
{author.data.image && (
<Image
src={author.data.image}
alt={author.data.title}
height={50}
width={50}
class="mr-2 h-6 w-6 rounded-full"
/>
)}
<span>{author.data.title}</span>
</a>
))
}
</li>
<li class="mx-3 flex items-center flex-wrap font-medium">
<BiCalendarEdit className="mr-1 h-5 w-5 text-gray-600" />
<>{dateFormat(date)}</>
</li>
<li class="mx-3 flex items-center flex-wrap">
<BiCategoryAlt className="mr-1 h-[18px] w-[18px] text-gray-600" />
<>
<ul>
{
categories.map((category: string, i: number) => (
<li class="inline-block">
<a
href={`/categories/${slugify(category)}`}
class="mr-2 hover:text-primary font-medium"
>
{humanize(category)}
{i !== post.data.categories.length - 1 && ","}
</a>
</li>
))
}
</ul>
</>
</li>
</ul>
</div>
<div class="col-12 mt-8 mb-2">
{
image && (
<Image
src={image}
height={500}
width={1000}
alt={title}
class="rounded-lg"
/>
)
}
</div>
<div class="md:col-10">
<div class="content mb-16 text-left">
<Content />
</div>
<div class="flex flex-wrap items-center justify-between">
<ul class="mr-4 mb-4 space-x-3">
{
tags.map((tag: string) => (
<li class="inline-block">
<a
href={`/tags/${slugify(tag)}`}
class="block rounded-lg bg-theme-light px-4 py-2 font-semibold text-dark text-sm hover:text-primary transition duration-300"
>
#{humanize(tag)}
</a>
</li>
))
}
</ul>
<Share
className="social-share mb-4"
title={title}
description={description}
slug={post.slug}
/>
</div>
<div class="content mb-16 text-left">
<script defer src="https://comment.currytech.online/comentario.js"></script>
<comentario-comments></comentario-comments>
</div>
</div>
</article>
</div>
</section>
<!-- similar posts -->
{
similarPosts.length > 0 && (
<section class="section pt-0">
<div class="container">
<h2 class="mb-8 text-center h3">Similar Posts</h2>
<SimilarPosts posts={similarPosts.slice(0, 3)} />
</div>
</section>
)
}

87
src/layouts/Posts.astro Normal file
View File

@@ -0,0 +1,87 @@
---
import { Image } from "astro:assets";
import config from "@/config/config.json";
import { getSinglePage } from "@/lib/contentParser.astro";
import dateFormat from "@/lib/utils/dateFormat";
import { humanize,slugify } from "@/lib/utils/textConverter";
import { BiCalendarEdit,BiCategoryAlt } from "react-icons/bi";
const authors = await getSinglePage("authors");
const { summary_length } = config.settings;
const { className, posts, fluid } = Astro.props;
---
<div class={`row gy-5 gx-4 ${className} ${posts.length == 1 ? "justify-center" : ""} `}>
{
posts.map((post: any, i: number) => (
<div class={i === 0 && fluid != false ? "col-12" : "col-12 sm:col-6"}>
{post.data.image && (
<a href={`/${post.slug}`} class="rounded-lg block hover:text-primary overflow-hidden group">
<Image
class="group-hover:scale-[1.03] transition duration-300 w-full"
src={post.data.image}
alt={post.data.title}
width={i === 0 ? 925 : 445}
height={i === 0 ? 475 : 230}
/>
</a>
)}
<ul class="mt-6 mb-4 flex flex-wrap items-center text-text">
<li class="mr-5">
{authors
.filter((author) =>
post.data.authors
.map((author: string) => slugify(author))
.includes(slugify(author.data.title))
)
.map((author) => (
<a
href={`/authors/${slugify(author.data.title)}`}
class="flex items-center hover:text-primary font-medium"
>
{author.data.image && (
<Image
src={author.data.image}
alt={author.data.title}
height={50}
width={50}
class="mr-2 h-6 w-6 rounded-full"
/>
)}
<span>{author.data.title}</span>
</a>
))}
</li>
<li class="mr-5 flex items-center flex-wrap font-medium">
<BiCalendarEdit className="mr-1 h-5 w-5 text-gray-600" />
<>{dateFormat(post.data.date)}</>
</li>
<li class="mr-5 flex items-center flex-wrap">
<BiCategoryAlt className="mr-1 h-[18px] w-[18px] text-gray-600" />
<>
<ul>
{post.data.categories.map((category: string, i: number) => (
<li class="inline-block">
<a
href={`/categories/${slugify(category)}`}
class="mr-2 hover:text-primary font-medium"
>
{humanize(category)}{i !== post.data.categories.length - 1 && ","}
</a>
</li>
))}
</ul>
</>
</li>
</ul>
<h3 class="mb-4">
<a href={`/${post.slug}`} class="block hover:text-primary transition duration-300">
{post.data.title}
</a>
</h3>
<p class="text-text">
{post.body?.slice(0, Number(i === 0 && fluid != false ? summary_length * 2 : summary_length))}...
</p>
</div>
))
}
</div>

152
src/layouts/SearchBar.tsx Normal file
View File

@@ -0,0 +1,152 @@
import React from "react";
import config from "@/config/config.json";
import dateFormat from "@/lib/utils/dateFormat";
import { humanize, slugify } from "@/lib/utils/textConverter";
import Fuse from "fuse.js";
import { useEffect, useRef, useState } from "react";
import { BiCalendarEdit, BiCategoryAlt } from "react-icons/bi";
const { summary_length } = config.settings;
export type SearchItem = {
slug: string;
data: any;
content: any;
};
interface Props {
searchList: SearchItem[];
}
interface SearchResult {
item: SearchItem;
refIndex: number;
}
export default function SearchBar({ searchList }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputVal, setInputVal] = useState("");
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(
null,
);
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
setInputVal(e.currentTarget.value);
};
const fuse = new Fuse(searchList, {
keys: ["data.title", "data.categories", "data.tags"],
includeMatches: true,
minMatchCharLength: 2,
threshold: 0.5,
});
useEffect(() => {
const searchUrl = new URLSearchParams(window.location.search);
const searchStr = searchUrl.get("q");
if (searchStr) setInputVal(searchStr);
setTimeout(function () {
inputRef.current!.selectionStart = inputRef.current!.selectionEnd =
searchStr?.length || 0;
}, 50);
}, []);
useEffect(() => {
let inputResult = inputVal.length > 2 ? fuse.search(inputVal) : [];
setSearchResults(inputResult);
if (inputVal.length > 0) {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("q", inputVal);
const newRelativePathQuery =
window.location.pathname + "?" + searchParams.toString();
history.pushState(null, "", newRelativePathQuery);
} else {
history.pushState(null, "", window.location.pathname);
}
}, [inputVal]);
return (
<div className="min-h-[45vh]">
<input
className="form-input w-full text-center"
placeholder="Type here to Search posts"
type="text"
name="search"
value={inputVal}
onChange={handleChange}
autoComplete="off"
autoFocus
ref={inputRef}
/>
{inputVal.length > 1 && (
<div className="my-6 text-center">
Found {searchResults?.length}
{searchResults?.length && searchResults?.length === 1
? " result"
: " results"}{" "}
for '{inputVal}'
</div>
)}
<div className="row">
{searchResults?.map(({ item }) => (
<div key={item.slug} className={"col-12 mb-8 sm:col-6"}>
{item.data.image && (
<a
href={`/${item.slug}`}
className="rounded-lg block hover:text-primary overflow-hidden group"
>
<img
className="group-hover:scale-[1.03] transition duration-300 w-full"
src={item.data.image}
alt={item.data.title}
width={445}
height={230}
/>
</a>
)}
<ul className="mt-6 mb-4 flex flex-wrap items-center text-text">
<li className="mr-5 flex items-center flex-wrap font-medium">
<BiCalendarEdit className="mr-1 h-5 w-5 text-gray-600" />
<>{dateFormat(item.data.date)}</>
</li>
<li className="mr-5 flex items-center flex-wrap">
<BiCategoryAlt className="mr-1 h-[18px] w-[18px] text-gray-600" />
<>
<ul>
{item.data.categories.map((category: string, i: number) => (
<li key={i} className="inline-block">
<a
href={`/categories/${slugify(category)}`}
className="mr-2 hover:text-primary font-medium"
>
{humanize(category)}
{i !== item.data.categories.length - 1 && ","}
</a>
</li>
))}
</ul>
</>
</li>
</ul>
<h3 className="mb-2">
<a
href={`/${item.slug}`}
className="block hover:text-primary transition duration-300"
>
{item.data.title}
</a>
</h3>
<p className="text-text">
{item.content?.slice(0, Number(summary_length))}...
</p>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,39 @@
---
import { Image } from "astro:assets";
import config from "@/config/config.json";
const { src } = Astro.props;
const {
logo,
logo_width,
logo_height,
logo_text,
title,
}: {
logo: string;
logo_width: any;
logo_height: any;
logo_text: string;
title: string;
} = config.site;
---
<a href="/" class="navbar-brand block">
{
src || logo ? (
<Image
width={logo_width.replace("px", "") * 2}
height={logo_height.replace("px", "") * 2}
src={src ? src : logo}
alt={title}
style={{
height: logo_height.replace("px", "") + "px",
width: logo_width.replace("px", "") + "px",
}}
/>
) : logo_text ? (
logo_text
) : (
title
)
}
</a>

View File

@@ -0,0 +1,126 @@
---
const { section, currentPage, totalPages } = Astro.props;
const indexPageLink = currentPage === 2;
const hasPrevPage = currentPage > 1;
const hasNextPage = totalPages > currentPage;
let pageList = [];
for (let i = 1; i <= totalPages; i++) {
pageList.push(i);
}
---
{
totalPages > 1 && (
<nav class="mb-4 flex justify-center space-x-2 text-center" aria-label="Pagination">
{/* previous */}
{hasPrevPage ? (
<a
href={
indexPageLink
? `${section ? "/" + section : "/"}`
: `${section ? "/" + section : ""}/page/${currentPage - 1}`
}
class="border border-primary hover:bg-primary hover:text-white rounded-md h-10 w-10 leading-[36px] font-semibold text-dark flex items-center justify-center transition duration-200"
>
<span class="sr-only">Previous</span>
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</a>
) : (
<span class="border border-gray-400 opacity-40 rounded-md h-10 w-10 leading-[36px] font-semibold text-dark flex items-center justify-center pointer-events-none">
<span class="sr-only">Previous</span>
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</span>
)}
{/* page index */}
{pageList.map((pagination, i) =>
pagination === currentPage ? (
<span
aria-current="page"
class={`border border-primary bg-primary rounded-md h-10 w-10 leading-[35px] font-semibold text-white`}
>
{pagination}
</span>
) : (
<a
href={
i === 0
? `${section ? "/" + section : "/"}`
: `${section ? "/" + section : ""}/page/${pagination}`
}
aria-current="page"
class={`border border-primary hover:bg-primary hover:text-white rounded-md h-10 w-10 leading-[36px] font-semibold text-dark flex items-center justify-center transition duration-200`}
>
{pagination}
</a>
)
)}
{/* next page */}
{hasNextPage ? (
<a
href={`${section ? "/" + section : ""}/page/${currentPage + 1}`}
class="border border-primary hover:bg-primary hover:text-white rounded-md h-10 w-10 leading-[36px] font-semibold text-dark flex items-center justify-center transition duration-200"
>
<span class="sr-only">Next</span>
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
</a>
) : (
<span class="border border-gray-400 opacity-40 rounded-md h-10 w-10 leading-[36px] font-semibold text-dark flex items-center justify-center pointer-events-none">
<span class="sr-only">Next</span>
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
</span>
)}
</nav>
)
}

View File

@@ -0,0 +1,55 @@
---
import config from "@/config/config.json";
import {
IoLogoFacebook,
IoLogoLinkedin,
IoLogoPinterest,
IoLogoTwitter,
} from "react-icons/io5";
const { base_url } = config.site;
const { title, description, slug, className } = Astro.props;
---
<ul class={`${className}`}>
<li class="inline-block">
<a
aria-label="facebook share button"
href={`https://facebook.com/sharer/sharer.php?u=${base_url}/${slug}`}
target="_blank"
rel="noreferrer noopener"
>
<IoLogoFacebook />
</a>
</li>
<li class="inline-block">
<a
aria-label="twitter share button"
href={`https://twitter.com/intent/tweet/?text=${title}&amp;url=${base_url}/${slug}`}
target="_blank"
rel="noreferrer noopener"
>
<IoLogoTwitter />
</a>
</li>
<li class="inline-block">
<a
aria-label="linkedin share button"
href={`https://www.linkedin.com/shareArticle?mini=true&url=${base_url}/${slug}&title=${title}&summary=${description}&source=${base_url}`}
target="_blank"
rel="noreferrer noopener"
>
<IoLogoLinkedin />
</a>
</li>
<li class="inline-block">
<a
aria-label="pinterest share button"
href={`https://pinterest.com/pin/create/button/?url=${base_url}/${slug}&media=&description=${description}`}
target="_blank"
rel="noreferrer noopener"
>
<IoLogoPinterest />
</a>
</li>
</ul>

View File

@@ -0,0 +1,62 @@
---
import { Image } from "astro:assets";
import dateFormat from "@/lib/utils/dateFormat";
import { humanize, slugify } from "@/lib/utils/textConverter";
import { BiCalendarEdit, BiCategoryAlt } from "react-icons/bi";
const { posts } = Astro.props;
---
<div class="row gy-4 justify-center">
{
posts.map((post: any, i: number) => (
<div class={`col-12 sm:col-6 md:col-4 ${i === 2 && "hidden md:block"}`}>
{post.data.image && (
<a
href={`/${post.slug}`}
class="rounded-lg block hover:text-primary overflow-hidden group"
>
<Image
class="group-hover:scale-[1.05] transition duration-300 w-full"
src={post.data.image}
alt={post.data.title}
width={445}
height={230}
/>
</a>
)}
<ul class="mt-4 text-text flex flex-wrap items-center text-sm">
<li class="mb-2 mr-4 flex items-center flex-wrap font-medium">
<BiCalendarEdit className="mr-1 h-[16px] w-[16px] text-gray-600" />
<>{dateFormat(post.data.date)}</>
</li>
<li class="mb-2 mr-4 flex items-center flex-wrap">
<BiCategoryAlt className="mr-1 h-[16px] w-[16px] text-gray-600" />
<>
<ul>
{post.data.categories.map((category: string, i: number) => (
<li class="inline-block">
<a
href={`/categories/${slugify(category)}`}
class="mr-2 hover:text-primary font-medium"
>
{humanize(category)}
{i !== post.data.categories.length - 1 && ","}
</a>
</li>
))}
</ul>
</>
</li>
</ul>
<h3 class="h5">
<a
href={`/${post.slug}`}
class="block hover:text-primary transition duration-300"
>
{post.data.title}
</a>
</h3>
</div>
))
}
</div>

View File

@@ -0,0 +1,498 @@
---
const { source, className } = Astro.props;
import {
IoCall,
IoGlobeOutline,
IoLocation,
IoLogoBehance,
IoLogoBitbucket,
IoLogoCodepen,
IoLogoDiscord,
IoLogoDribbble,
IoLogoFacebook,
IoLogoFoursquare,
IoLogoGithub,
IoLogoGitlab,
IoLogoInstagram,
IoLogoLinkedin,
IoLogoMastodon,
IoLogoMedium,
IoLogoPinterest,
IoLogoReddit,
IoLogoRss,
IoLogoSkype,
IoLogoSlack,
IoLogoSnapchat,
IoLogoSoundcloud,
IoLogoTiktok,
IoLogoTumblr,
IoLogoTwitter,
IoLogoVimeo,
IoLogoVk,
IoLogoWhatsapp,
IoLogoYoutube,
IoMail,
} from "react-icons/io5";
const {
facebook,
twitter,
mastodon,
instagram,
youtube,
linkedin,
github,
gitlab,
discord,
slack,
medium,
codepen,
bitbucket,
dribbble,
behance,
pinterest,
soundcloud,
tumblr,
reddit,
vk,
whatsapp,
snapchat,
vimeo,
tiktok,
foursquare,
rss,
email,
phone,
address,
skype,
website,
} = source;
---
<ul class={className}>
{
facebook && (
<li class="inline-block">
<a
aria-label="facebook"
href={facebook}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoFacebook />
</a>
</li>
)
}
{
twitter && (
<li class="inline-block">
<a
aria-label="twitter"
href={twitter}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoTwitter />
</a>
</li>
)
}
{
mastodon && (
<li class="inline-block">
<a
aria-label="mastodon"
href={mastodon}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoMastodon />
</a>
</li>
)
}
{
instagram && (
<li class="inline-block">
<a
aria-label="instagram"
href={instagram}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoInstagram />
</a>
</li>
)
}
{
youtube && (
<li class="inline-block">
<a
aria-label="youtube"
href={youtube}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoYoutube />
</a>
</li>
)
}
{
linkedin && (
<li class="inline-block">
<a
aria-label="linkedin"
href={linkedin}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoLinkedin />
</a>
</li>
)
}
{
github && (
<li class="inline-block">
<a
aria-label="github"
href={github}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoGithub />
</a>
</li>
)
}
{
gitlab && (
<li class="inline-block">
<a
aria-label="gitlab"
href={gitlab}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoGitlab />
</a>
</li>
)
}
{
discord && (
<li class="inline-block">
<a
aria-label="discord"
href={discord}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoDiscord />
</a>
</li>
)
}
{
slack && (
<li class="inline-block">
<a
aria-label="slack"
href={slack}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoSlack />
</a>
</li>
)
}
{
medium && (
<li class="inline-block">
<a
aria-label="medium"
href={medium}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoMedium />
</a>
</li>
)
}
{
codepen && (
<li class="inline-block">
<a
aria-label="codepen"
href={codepen}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoCodepen />
</a>
</li>
)
}
{
bitbucket && (
<li class="inline-block">
<a
aria-label="bitbucket"
href={bitbucket}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoBitbucket />
</a>
</li>
)
}
{
dribbble && (
<li class="inline-block">
<a
aria-label="dribbble"
href={dribbble}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoDribbble />
</a>
</li>
)
}
{
behance && (
<li class="inline-block">
<a
aria-label="behance"
href={behance}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoBehance />
</a>
</li>
)
}
{
pinterest && (
<li class="inline-block">
<a
aria-label="pinterest"
href={pinterest}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoPinterest />
</a>
</li>
)
}
{
soundcloud && (
<li class="inline-block">
<a
aria-label="soundcloud"
href={soundcloud}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoSoundcloud />
</a>
</li>
)
}
{
tumblr && (
<li class="inline-block">
<a
aria-label="tumblr"
href={tumblr}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoTumblr />
</a>
</li>
)
}
{
reddit && (
<li class="inline-block">
<a
aria-label="reddit"
href={reddit}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoReddit />
</a>
</li>
)
}
{
vk && (
<li class="inline-block">
<a
aria-label="vk"
href={vk}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoVk />
</a>
</li>
)
}
{
whatsapp && (
<li class="inline-block">
<a
aria-label="whatsapp"
href={whatsapp}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoWhatsapp />
</a>
</li>
)
}
{
snapchat && (
<li class="inline-block">
<a
aria-label="snapchat"
href={snapchat}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoSnapchat />
</a>
</li>
)
}
{
vimeo && (
<li class="inline-block">
<a
aria-label="vimeo"
href={vimeo}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoVimeo />
</a>
</li>
)
}
{
tiktok && (
<li class="inline-block">
<a
aria-label="tiktok"
href={tiktok}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoTiktok />
</a>
</li>
)
}
{
foursquare && (
<li class="inline-block">
<a
aria-label="foursquare"
href={foursquare}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoFoursquare />
</a>
</li>
)
}
{
skype && (
<li class="inline-block">
<a
aria-label="skype"
href={skype}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoSkype />
</a>
</li>
)
}
{
website && (
<li class="inline-block">
<a
aria-label="website"
href={website}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoGlobeOutline />
</a>
</li>
)
}
{
rss && (
<li class="inline-block">
<a
aria-label="rss feed"
href={rss}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLogoRss />
</a>
</li>
)
}
{
email && (
<li class="inline-block">
<a aria-label="email" href={`mailto:${email}`}>
<IoMail />
</a>
</li>
)
}
{
phone && (
<li class="inline-block">
<a aria-label="telephone" href={`tel:${phone}`}>
<IoCall />
</a>
</li>
)
}
{
address && (
<li class="inline-block">
<a
aria-label="location"
href={address}
target="_blank"
rel="noopener noreferrer nofollow"
>
<IoLocation />
</a>
</li>
)
}
</ul>

View File

@@ -0,0 +1,15 @@
---
---
{
process.env.NODE_ENV === "development" && (
<div class="fixed top-0 left-0 z-50 flex w-[30px] items-center justify-center bg-gray-200 py-[2.5px] text-[12px] uppercase text-black sm:bg-red-200 md:bg-yellow-200 lg:bg-green-200 xl:bg-blue-200 2xl:bg-pink-200">
<span class="block sm:hidden">all</span>
<span class="hidden sm:block md:hidden">sm</span>
<span class="hidden md:block lg:hidden">md</span>
<span class="hidden lg:block xl:hidden">lg</span>
<span class="hidden xl:block 2xl:hidden">xl</span>
<span class="hidden 2xl:block">2xl</span>
</div>
)
}

View File

@@ -0,0 +1,28 @@
---
import Social from "@/components/Social.astro";
import config from "@/config/config.json";
import menu from "@/config/menu.json";
import social from "@/config/social.json";
import { markdownify } from "@/lib/utils/textConverter";
---
<footer class="section bg-theme-dark">
<div class="container text-center">
{/* footer menu */}
<ul class="mb-8 space-x-4">
{
menu.footer.map((menu) => (
<li class="inline-block">
<a href={menu.url} class="p-4 text-white/75 hover:text-white transition-all">
{menu.name}
</a>
</li>
))
}
</ul>
{/* social icons */}
<Social source={social} className="social-icons mb-8" />
{/* copyright */}
<p set:html={markdownify(config.params.copyright)} class="text-white/75" />
</div>
</footer>

View File

@@ -0,0 +1,100 @@
---
import Logo from "@/components/Logo.astro";
import menu from "@/config/menu.json";
import { IoSearch } from "react-icons/io5";
export interface ChildNavigationLink {
name: string;
url: string;
}
export interface NavigationLink {
name: string;
url: string;
hasChildren?: boolean;
children?: ChildNavigationLink[];
}
const { main }: { main: NavigationLink[] } = menu;
---
<header class="header pt-6">
<nav class="navbar container">
<!-- logo -->
<div class="order-0">
<Logo />
</div>
<!-- navbar toggler -->
<input id="nav-toggle" type="checkbox" class="hidden" />
<label
id="show-button"
for="nav-toggle"
class="order-2 flex cursor-pointer items-center text-black md:order-1 md:hidden"
>
<svg class="h-6 fill-current" viewBox="0 0 20 20">
<title>Menu Open</title>
<path d="M0 3h20v2H0V3z m0 6h20v2H0V9z m0 6h20v2H0V0z"></path>
</svg>
</label>
<label
id="hide-button"
for="nav-toggle"
class="order-2 hidden cursor-pointer items-center text-black md:order-1"
>
<svg class="h-6 fill-current" viewBox="0 0 20 20">
<title>Menu Close</title>
<polygon
points="11 9 22 9 22 11 11 11 11 22 9 22 9 11 -2 11 -2 9 9 9 9 -2 11 -2"
transform="rotate(45 10 10)"></polygon>
</svg>
</label>
<!-- /navbar toggler -->
<ul
id="nav-menu"
class="navbar-nav order-3 hidden w-full md:order-1 md:flex md:w-auto"
>
{
main.map((menu) => (
<>
{menu.hasChildren ? (
<li class="nav-item nav-dropdown group relative cursor-pointer">
<span class="nav-link inline-flex items-center">
{menu.name}
<svg class="h-5 w-5 fill-current" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
</svg>
</span>
<ul class="nav-dropdown-list hidden group-hover:block md:invisible md:absolute md:block md:opacity-0 md:group-hover:visible md:group-hover:opacity-100">
{menu.children?.map((child) => (
<li class="nav-dropdown-item">
<a href={child.url} class="nav-dropdown-link">
{child.name}
</a>
</li>
))}
</ul>
</li>
) : (
<li class="nav-item">
<a href={menu.url} class="nav-link block">
{menu.name}
</a>
</li>
)}
</>
))
}
</ul>
<div class="order-1 ml-auto flex md:order-2 md:ml-0">
<a
class="mr-4 inline-block cursor-pointer p-2 text-xl text-dark transition hover:text-primary md:mr-0"
href="/search"
title="seach"
aria-label="seach"
>
<IoSearch />
</a>
</div>
</nav>
</header>

View File

@@ -0,0 +1,36 @@
import React, { useState } from "react";
const Accordion = ({
title,
children,
className,
}: {
title: string;
children: React.ReactNode;
className?: string;
}) => {
const [show, setShow] = useState(false);
return (
<div className={`accordion ${show && "active"} ${className}`}>
<button className="accordion-header" onClick={() => setShow(!show)}>
{title}
<svg
className="accordion-icon"
x="0px"
y="0px"
viewBox="0 0 512 512"
xmlSpace="preserve"
>
<path
fill="currentColor"
d="M505.755,123.592c-8.341-8.341-21.824-8.341-30.165,0L256.005,343.176L36.421,123.592c-8.341-8.341-21.824-8.341-30.165,0 s-8.341,21.824,0,30.165l234.667,234.667c4.16,4.16,9.621,6.251,15.083,6.251c5.462,0,10.923-2.091,15.083-6.251l234.667-234.667 C514.096,145.416,514.096,131.933,505.755,123.592z"
></path>
</svg>
</button>
<div className="accordion-content">{children}</div>
</div>
);
};
export default Accordion;

View File

@@ -0,0 +1,30 @@
import React from "react";
const Button = ({
label,
link,
style,
rel,
}: {
label: string;
link: string;
style?: string;
rel?: string;
}) => {
return (
<a
href={link}
target="_blank"
rel={`noopener noreferrer ${
rel ? (rel === "follow" ? "" : rel) : "nofollow"
}`}
className={`btn mb-4 me-4 hover:text-white no-underline ${
style === "outline" ? "btn-outline-primary" : "btn-primary"
}`}
>
{label}
</a>
);
};
export default Button;

View File

@@ -0,0 +1,85 @@
import { humanize } from "@/lib/utils/textConverter";
import React from "react";
function Notice({
type,
children,
}: {
type: string;
children: React.ReactNode;
}) {
return (
<div className={`notice ${type}`}>
<div className="notice-head">
{type === "tip" ? (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0ZM12 2.4C6.69807 2.4 2.4 6.69807 2.4 12C2.4 17.3019 6.69807 21.6 12 21.6C17.3019 21.6 21.6 17.3019 21.6 12C21.6 6.69807 17.3019 2.4 12 2.4ZM15.9515 7.55147L9.6 13.9029L8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515C5.88284 12.8201 5.88284 13.5799 6.35147 14.0485L8.75147 16.4485C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485L17.6485 9.24853C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147C17.1799 7.08284 16.4201 7.08284 15.9515 7.55147Z"
fill="currentColor"
/>
</svg>
) : type === "info" ? (
<svg
width="20"
height="20"
viewBox="0 0 18 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.16109 0.993016C9.97971 1.03952 10.6611 1.42989 11.0721 2.22339L17.7981 15.8014C18.4502 17.1739 17.4403 19.0208 15.7832 19.0474H2.23859C0.730337 19.0234 -0.507163 17.3108 0.231587 15.7864L7.08321 2.20877C7.21146 1.96502 7.26996 1.89452 7.38059 1.76664C7.82534 1.25102 8.31171 0.975016 9.16109 0.993016ZM9.05046 2.49189C8.79284 2.50464 8.55696 2.64902 8.42834 2.87327C6.06134 7.36539 3.77946 11.9036 1.56546 16.4734C1.36071 16.9328 1.71209 17.5223 2.22621 17.547C6.74871 17.6201 11.2731 17.6201 15.7956 17.547C16.2925 17.523 16.666 16.953 16.459 16.4783C14.2866 11.9093 12.0471 7.37102 9.72171 2.87814C9.58446 2.63402 9.38309 2.48739 9.05046 2.49189Z"
fill="currentColor"
/>
<path
d="M9.61323 13.2153H8.35773L8.21973 7.04688H9.75723L9.61323 13.2153ZM8.17773 15.1015C8.17773 14.8731 8.25161 14.6841 8.39973 14.5338C8.54823 14.3838 8.75036 14.3084 9.00648 14.3084C9.26298 14.3084 9.46511 14.3838 9.61323 14.5338C9.76136 14.6841 9.83561 14.8731 9.83561 15.1015C9.83561 15.3216 9.76323 15.5057 9.61923 15.6539C9.47486 15.802 9.27086 15.8762 9.00648 15.8762C8.74211 15.8762 8.53811 15.802 8.39373 15.6539C8.24973 15.5057 8.17773 15.3216 8.17773 15.1015Z"
fill="currentColor"
/>
</svg>
) : type === "warning" ? (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 0C15.522 0 20 4.478 20 10C20 15.522 15.522 20 10 20C4.478 20 0 15.522 0 10C0 4.478 4.478 0 10 0ZM10 2C5.589 2 2 5.589 2 10C2 14.411 5.589 18 10 18C14.411 18 18 14.411 18 10C18 5.589 14.411 2 10 2ZM12.293 6.293L13.707 7.707L11.414 10L13.707 12.293L12.293 13.707L10 11.414L7.707 13.707L6.293 12.293L8.586 10L6.293 7.707L7.707 6.293L10 8.586L12.293 6.293Z"
fill="currentColor"
/>
</svg>
) : (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
<p className="my-0 ml-1.5">{humanize(type)}</p>
</div>
<div className="notice-body">{children}</div>
</div>
);
}
export default Notice;

View File

@@ -0,0 +1,7 @@
import React from "react";
function Tab({ name, children }: { name: string; children: React.ReactNode }) {
return <div data-name={name}>{children}</div>;
}
export default Tab;

View File

@@ -0,0 +1,76 @@
import { marked } from "marked";
import React, { useEffect, useRef, useState } from "react";
marked.use({
mangle: false,
headerIds: false,
});
const Tabs = ({ children }: { children: React.ReactElement }) => {
const [active, setActive] = useState<number>(0);
const [defaultFocus, setDefaultFocus] = useState<boolean>(false);
const tabRefs: React.RefObject<HTMLElement[]> = useRef([]);
useEffect(() => {
if (defaultFocus) {
//@ts-ignore
tabRefs.current[active]?.focus();
} else {
setDefaultFocus(true);
}
}, [active]);
const tabLinks = Array.from(
children.props.value.matchAll(
/<div\s+data-name="([^"]+)"[^>]*>(.*?)<\/div>/gs,
),
(match: RegExpMatchArray) => ({ name: match[1], children: match[0] }),
);
const handleKeyDown = (
event: React.KeyboardEvent<EventTarget>,
index: number,
) => {
if (event.key === "Enter" || event.key === " ") {
setActive(index);
} else if (event.key === "ArrowRight") {
setActive((active + 1) % tabLinks.length);
} else if (event.key === "ArrowLeft") {
setActive((active - 1 + tabLinks.length) % tabLinks.length);
}
};
return (
<div className="tab">
<ul className="tab-nav">
{tabLinks.map(
(item: { name: string; children: string }, index: number) => (
<li
key={index}
className={`tab-nav-item ${index === active && "active"}`}
role="tab"
tabIndex={index === active ? 0 : -1}
onKeyDown={(event) => handleKeyDown(event, index)}
onClick={() => setActive(index)}
//@ts-ignore
ref={(ref) => (tabRefs.current[index] = ref)}
>
{item.name}
</li>
),
)}
</ul>
{tabLinks.map((item: { name: string; children: string }, i: number) => (
<div
className={active === i ? "tab-content block px-5" : "hidden"}
key={i}
dangerouslySetInnerHTML={{
__html: marked.parse(item.children),
}}
/>
))}
</div>
);
};
export default Tabs;

View File

@@ -0,0 +1,32 @@
import React from "react";
function Video({
title,
width = 500,
height = "auto",
src,
...rest
}: {
title: string;
width: number;
height: number | "auto";
src: string;
[key: string]: any;
}) {
return (
<video
className="overflow-hidden rounded-lg"
width={width}
height={height}
controls
{...rest}
>
<source
src={src.match(/^http/) ? src : `/videos/${src}`}
type="video/mp4"
/>
{title}
</video>
);
}
export default Video;

View File

@@ -0,0 +1,24 @@
import React from "react";
import LiteYouTubeEmbed from "react-lite-youtube-embed";
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
const Youtube = ({
id,
title,
...rest
}: {
id: string;
title: string;
[key: string]: any;
}) => {
return (
<LiteYouTubeEmbed
wrapperClass="yt-lite rounded-lg"
id={id}
title={title}
{...rest}
/>
);
};
export default Youtube;

View File

@@ -0,0 +1,16 @@
---
import {
getCollection,
type CollectionEntry,
type CollectionKey,
} from "astro:content";
export const getSinglePage = async <C extends CollectionKey>(
collectionName: C,
): Promise<CollectionEntry<C>[]> => {
const allPages = await getCollection(collectionName);
const removeIndex = allPages.filter((data) => data.id.match(/^(?!-)/));
const removeDrafts = removeIndex.filter((data) => !data.data.draft);
return removeDrafts;
};
---

View File

@@ -0,0 +1,33 @@
---
import { getSinglePage } from "@/lib/contentParser.astro";
import { slugify } from "@/lib/utils/textConverter";
// get taxonomy from frontmatter
export const getTaxonomy = async (collection: any, name: string) => {
const singlePages = await getSinglePage(collection);
const taxonomyPages = singlePages.map((page: any) => page.data[name]);
let taxonomies: string[] = [];
for (let i = 0; i < taxonomyPages.length; i++) {
const categoryArray = taxonomyPages[i];
for (let j = 0; j < categoryArray.length; j++) {
taxonomies.push(slugify(categoryArray[j])!);
}
}
const taxonomy = [...new Set(taxonomies)];
return taxonomy;
};
// get all taxonomies from frontmatter
export const getAllTaxonomy = async (collection: any, name: string) => {
const singlePages = await getSinglePage(collection);
const taxonomyPages = singlePages.map((page: any) => page.data[name]);
let taxonomies: string[] = [];
for (let i = 0; i < taxonomyPages.length; i++) {
const categoryArray = taxonomyPages[i];
for (let j = 0; j < categoryArray.length; j++) {
taxonomies.push(slugify(categoryArray[j])!);
}
}
return taxonomies;
};
---

View File

@@ -0,0 +1,12 @@
import { format } from "date-fns";
const dateFormat = (
date: Date | string,
pattern: string = "dd MMM, yyyy",
): string => {
const dateObj = new Date(date);
const output = format(dateObj, pattern);
return output;
};
export default dateFormat;

View File

@@ -0,0 +1,40 @@
// content reading
const readingTime = (content: string) => {
const WPS = 275 / 60;
let images = 0;
const regex = /\w/;
let words = content.split(" ").filter((word) => {
if (word.includes("<img")) {
images += 1;
}
return regex.test(word);
}).length;
let imageAdjust = images * 4;
let imageSecs = 0;
let imageFactor = 12;
while (images) {
imageSecs += imageFactor;
if (imageFactor > 3) {
imageFactor -= 1;
}
images -= 1;
}
const minutes = Math.ceil(((words - imageAdjust) / WPS + imageSecs) / 60);
if (minutes < 10) {
if (minutes < 2) {
return "0" + minutes + ` Min read`;
} else {
return "0" + minutes + ` Mins read`;
}
} else {
return minutes + ` Mins read`;
}
};
export default readingTime;

View File

@@ -0,0 +1,36 @@
// similer products
const similerItems = (currentItem: any, allItems: any, slug: string) => {
let categories: [] = [];
let tags: [] = [];
// set categories
if (currentItem.data.categories.length > 0) {
categories = currentItem.data.categories;
}
// set tags
if (currentItem.data.tags.length > 0) {
tags = currentItem.data.tags;
}
// filter by categories
const filterByCategories = allItems.filter(
(item: { data: { categories: string } }) =>
categories.find((category) => item.data.categories.includes(category))
);
// filter by tags
const filterByTags = allItems.filter((item: { data: { tags: string } }) =>
tags.find((tag) => item.data.tags.includes(tag))
);
// merged after filter
const mergedItems = [...new Set([...filterByCategories, ...filterByTags])];
// filter by slug
const filterBySlug = mergedItems.filter((product) => product.slug !== slug);
return filterBySlug;
};
export default similerItems;

View File

@@ -0,0 +1,25 @@
// sort by date
export const sortByDate = (array: any[]) => {
const sortedArray = array.sort(
(a:any, b:any) =>
new Date(b.data.date && b.data.date) -
new Date(a.data.date && a.data.date)
);
return sortedArray;
};
// sort product by weight
export const sortByWeight = (array: any[]) => {
const withWeight = array.filter(
(item: { data: { weight: any } }) => item.data.weight
);
const withoutWeight = array.filter(
(item: { data: { weight: any } }) => !item.data.weight
);
const sortedWeightedArray = withWeight.sort(
(a: { data: { weight: number } }, b: { data: { weight: number } }) =>
a.data.weight - b.data.weight
);
const sortedArray = [...new Set([...sortedWeightedArray, ...withoutWeight])];
return sortedArray;
};

View File

@@ -0,0 +1,8 @@
import { slugify } from "@/lib/utils/textConverter";
const taxonomyFilter = (posts: any[], name: string, key: any) =>
posts.filter((post) =>
post.data[name].map((name: string) => slugify(name)).includes(key)
);
export default taxonomyFilter;

View File

@@ -0,0 +1,57 @@
import { slug } from 'github-slugger';
import { marked } from "marked";
// slugify
export const slugify = (content: string) => {
if (!content) return null;
return slug(content);
};
// markdownify
export const markdownify = (content: string) => {
if (!content) return null;
return marked.parseInline(content);
};
// humanize
export const humanize = (content: string) => {
if (!content) return null;
return content
.replace(/^[\s_]+|[\s_]+$/g, "")
.replace(/[_\s]+/g, " ")
.replace(/^[a-z]/, function (m) {
return m.toUpperCase();
});
};
// plainify
export const plainify = (content: string) => {
if (!content) return null;
const filterBrackets = content.replace(/<\/?[^>]+(>|$)/gm, "");
const filterSpaces = filterBrackets.replace(/[\r\n]\s*[\r\n]/gm, "");
const stripHTML = htmlEntityDecoder(filterSpaces);
return stripHTML;
};
// strip entities for plainify
const htmlEntityDecoder = (htmlWithEntities: string): string => {
let entityList: { [key: string]: string } = {
"&nbsp;": " ",
"&lt;": "<",
"&gt;": ">",
"&amp;": "&",
"&quot;": '"',
"&#39;": "'",
};
let htmlWithoutEntities: string = htmlWithEntities.replace(
/(&amp;|&lt;|&gt;|&quot;|&#39;)/g,
(entity: string): string => {
return entityList[entity];
}
);
return htmlWithoutEntities;
};

20
src/pages/404.astro Normal file
View File

@@ -0,0 +1,20 @@
---
import Base from "@/layouts/Base.astro";
import { markdownify } from "@/lib/utils/textConverter";
import { getEntryBySlug } from "astro:content";
const entry = await getEntryBySlug("pages", "404");
const { Content } = await entry.render();
---
<Base title={entry.data.title}>
<section class="section">
<div class="container">
<div class="flex h-[40vh] items-center justify-center">
<div class="text-center">
<h1 class="mb-4" set:html={markdownify(entry.data.title)} />
<Content />
</div>
</div>
</div>
</section>
</Base>

42
src/pages/[regular].astro Normal file
View File

@@ -0,0 +1,42 @@
---
import Base from "@/layouts/Base.astro";
import Default from "@/layouts/Default.astro";
import PostSingle from "@/layouts/PostSingle.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import type { TPost } from "@/types";
const getPosts = (await getSinglePage("posts")) as TPost[];
const postsSlug = getPosts.map((item) => item.slug);
export async function getStaticPaths() {
const posts = await getSinglePage("posts");
const pages = await getSinglePage("pages");
const allPages = [...pages, ...posts];
const paths = allPages.map((page: any) => ({
params: {
regular: page.slug,
},
props: { page },
}));
return paths;
}
const { page } = Astro.props;
const { title, meta_title, description, image } = page.data;
---
<Base
title={title}
meta_title={meta_title}
description={description}
image={image}
>
{
postsSlug.includes(page.slug) ? (
<PostSingle post={page} />
) : (
<Default data={page} />
)
}
</Base>

66
src/pages/about.astro Normal file
View File

@@ -0,0 +1,66 @@
---
import { Image } from "astro:assets";
import Base from "@/layouts/Base.astro";
import { markdownify } from "@/lib/utils/textConverter";
import { getEntryBySlug } from "astro:content";
const entry = await getEntryBySlug("about", "index");
const { Content } = await entry.render();
const { title, description, meta_title, image, what_i_do } = entry.data;
---
<Base
title={title}
meta_title={meta_title}
description={description}
image={image}
>
<section class="section">
<div class="container">
<div class="row md:gx-4">
<div class="sm:col-5 md:col-4">
{
image && (
<div class="img-cover mb-8">
<Image
src={image}
width={295}
height={395}
alt={title}
class="rounded-lg w-full"
/>
</div>
)
}
</div>
<div class="sm:col-7 md:col-8">
<h1 set:html={markdownify(title)} class="h3 mb-8" />
<div class="content">
<Content />
</div>
<a href="/contact" class="btn btn-primary text-white py-2"
>Get In Touch</a
>
</div>
</div>
</div>
</section>
<section class="section pt-0">
<div class="container">
<h3 class="page-heading mb-20">{what_i_do.title}</h3>
<div class="row justify-center gy-4 text-center">
{
what_i_do.items.map((item: any) => (
<div class="lg:col-4 md:col-6">
<i class="{{.icon}} fa-3x text-primary mb-4" />
<h4 class="text-dark font-weight-700 mb-3">{item.title}</h4>
<p>{item.description}</p>
</div>
))
}
</div>
</div>
</section>
</Base>

View File

@@ -0,0 +1,110 @@
---
import { Image } from "astro:assets";
import AuthorSingle from "@/layouts/AuthorSingle.astro";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import dateFormat from "@/lib/utils/dateFormat";
import { sortByDate } from "@/lib/utils/sortFunctions";
import { humanize, slugify } from "@/lib/utils/textConverter";
import { BiCalendarEdit, BiCategoryAlt } from "react-icons/bi";
export async function getStaticPaths() {
const authors = await getSinglePage("authors");
const paths = authors.map((author: any) => ({
params: {
single: author.slug,
},
props: { author },
}));
return paths;
}
const { author } = Astro.props;
const { title, meta_title, description, image } = author.data;
// Author Posts
const posts = await getSinglePage("posts");
const sortPostsByDate = sortByDate(posts);
const currentPosts = sortPostsByDate.filter((post) => {
return post.data.authors
.map((author: string) => slugify(author))
.includes(slugify(title));
});
---
<Base
title={title}
meta_title={meta_title}
description={description}
image={image}
>
<AuthorSingle author={author} />
{
currentPosts.length > 0 && (
<section class="section pt-0">
<div class="container">
<h2 class="mb-8 text-center h3">Recent Posts</h2>
<div
class={`row gy-4 ${currentPosts.length < 3 ? "justify-center" : ""}`}
>
{currentPosts.map((post: any, i: number) => (
<div class="col-12 sm:col-6 lg:col-4">
{post.data.image && (
<a
href={`/${post.slug}`}
class="rounded-lg block hover:text-primary overflow-hidden group"
>
<Image
class="group-hover:scale-[1.05] transition duration-300 w-full"
src={post.data.image}
alt={post.data.title}
width={445}
height={230}
/>
</a>
)}
<ul class="mt-4 text-text flex flex-wrap items-center text-sm">
<li class="mb-2 mr-4 flex items-center flex-wrap font-medium">
<BiCalendarEdit className="mr-1 h-[16px] w-[16px] text-gray-600" />
<>{dateFormat(post.data.date)}</>
</li>
<li class="mb-2 mr-4 flex items-center flex-wrap">
<BiCategoryAlt className="mr-1 h-[16px] w-[16px] text-gray-600" />
<>
<ul>
{post.data.categories.map(
(category: string, i: number) => (
<li class="inline-block">
<a
href={`/categories/${slugify(category)}`}
class="mr-2 hover:text-primary font-medium"
>
{humanize(category)}
{i !== post.data.categories.length - 1 && ","}
</a>
</li>
),
)}
</ul>
</>
</li>
</ul>
<h3 class="h5">
<a
href={`/${post.slug}`}
class="block hover:text-primary transition duration-300"
>
{post.data.title}
</a>
</h3>
</div>
))}
</div>
</div>
</section>
)
}
</Base>

View File

@@ -0,0 +1,24 @@
---
import config from "@/config/config.json";
import Authors from "@/layouts/Authors.astro";
import Base from "@/layouts/Base.astro";
import Pagination from "@/layouts/components/Pagination.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { sortByDate } from "@/lib/utils/sortFunctions";
import { markdownify } from "@/lib/utils/textConverter";
const authors = await getSinglePage("authors");
const sortedPosts = sortByDate(authors);
const totalPages = Math.ceil(authors.length / config.settings.pagination);
const currentPosts = sortedPosts.slice(0, config.settings.pagination);
---
<Base title={"Authors"}>
<section class="section">
<div class="container text-center">
<h1 set:text={markdownify("Authors")} class="page-heading h2" />
<Authors authors={currentPosts} />
<Pagination section={"authors"} currentPage={1} totalPages={totalPages} />
</div>
</section>
</Base>

View File

@@ -0,0 +1,47 @@
---
import config from "@/config/config.json";
import Authors from "@/layouts/Authors.astro";
import Base from "@/layouts/Base.astro";
import Pagination from "@/layouts/components/Pagination.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { sortByDate } from "@/lib/utils/sortFunctions";
import { markdownify } from "@/lib/utils/textConverter";
export async function getStaticPaths() {
const authors = await getSinglePage("authors");
const totalPages = Math.ceil(authors.length / config.settings.pagination);
const paths = [];
for (let i = 1; i < totalPages; i++) {
paths.push({
params: {
slug: (i + 1).toString(),
},
});
}
return paths;
}
const { slug } = Astro.params;
const authors = await getSinglePage("authors");
const sortedPosts = sortByDate(authors);
const totalPages = Math.ceil(authors.length / config.settings.pagination);
const currentPage = slug && !isNaN(Number(slug)) ? Number(slug) : 1;
const indexOfLastPost = currentPage * config.settings.pagination;
const indexOfFirstPost = indexOfLastPost - config.settings.pagination;
const currentPosts = sortedPosts.slice(indexOfFirstPost, indexOfLastPost);
---
<Base title={"Authors"}>
<section class="section">
<div class="container text-center">
<h1 set:text={markdownify("Authors")} class="page-heading h2" />
<Authors authors={currentPosts} />
<Pagination
section={"authors"}
currentPage={currentPage}
totalPages={totalPages}
/>
</div>
</section>
</Base>

View File

@@ -0,0 +1,35 @@
---
import Base from "@/layouts/Base.astro";
import Posts from "@/layouts/Posts.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import taxonomyFilter from "@/lib/utils/taxonomyFilter";
import { humanize } from "@/lib/utils/textConverter";
export async function getStaticPaths() {
const categories = await getTaxonomy("posts", "categories");
return categories.map((category) => {
return {
params: { category },
};
});
}
const { category } = Astro.params;
const posts = await getSinglePage("posts");
const filterByCategory = taxonomyFilter(posts, "categories", category);
const title = humanize(category || "");
---
<Base title={title || "Category"}>
<div class="section">
<div class="container">
<p class="text-center mb-4">Showing Posts From</p>
<h1 class="h2 mb-16 text-center text-primary">{title}</h1>
<Posts posts={filterByCategory} fluid={false} />
</div>
</div>
</Base>

View File

@@ -0,0 +1,32 @@
---
import Base from "@/layouts/Base.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import { humanize } from "@/lib/utils/textConverter";
const categories = await getTaxonomy("posts", "categories");
import { BiCategoryAlt } from "react-icons/bi";
---
<Base title={"Categories"}>
<section class="section">
<div class="container text-center">
<h1 class="h2 page-heading">Categories</h1>
<ul class="space-x-4">
{
categories.map((category) => (
<li class="inline-block">
<a
href={`/categories/${category}`}
class="rounded-lg bg-theme-light px-4 py-2 text-dark transition hover:bg-primary hover:text-white flex items-center group"
>
<BiCategoryAlt className="mr-1 text-primary group-hover:text-white transition h-6 w-6 scale-75" />
<>{humanize(category || "")}</>
</a>
</li>
))
}
</ul>
</div>
</section>
</Base>

80
src/pages/contact.astro Normal file
View File

@@ -0,0 +1,80 @@
---
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { markdownify } from "@/lib/utils/textConverter";
import { getEntryBySlug } from "astro:content";
import { FaAddressCard, FaEnvelope, FaPhoneAlt } from "react-icons/fa";
const entry = await getEntryBySlug("pages", "contact");
const { contact_form_action } = config.params;
const { address, email, phone } = config.contactinfo;
const { title, description, meta_title, image } = entry.data;
---
<Base
title={title}
meta_title={meta_title}
description={description}
image={image}
>
<section class="section">
<div class="container">
<h1 set:html={markdownify(title)} class="h2 page-heading" />
<div class="row md:gx-5 gy-5">
<div class="sm:col-5 md:col-4">
<p class="mb-8 text-2xl font-bold text-theme-dark">Contact Info</p>
<ul class="flex flex-col space-y-8">
<li>
<div class="flex text-theme-dark items-center text-xl">
<FaAddressCard className="mr-3 text-primary" />
<p class="font-semibold">Address</p>
</div>
<p set:html={markdownify(address)} class="mt-2 leading-5 pl-8" />
</li>
<li>
<div class="flex text-theme-dark items-center text-xl">
<FaEnvelope className="mr-3 text-primary" />
<p class="font-semibold">Email</p>
</div>
<p set:html={markdownify(email)} class="mt-2 leading-5 pl-8 content" />
</li>
<li>
<div class="flex text-theme-dark items-center text-xl">
<FaPhoneAlt className="mr-3 text-primary" />
<p class="font-semibold">Phone</p>
</div>
<p set:html={markdownify(phone)} class="mt-2 leading-5 pl-8" />
</li>
</ul>
</div>
<div class="sm:col-7 md:col-8">
<form class="contact-form row gy-2 justify-center" method="POST" action={contact_form_action}>
<div class="lg:col-6">
<label class="mb-2 block" for="name">Name <span class="text-red-600">*</span></label>
<input class="form-input w-full" name="name" type="text" required />
</div>
<div class="lg:col-6">
<label class="mb-2 block" for="email">Email <span class="text-red-600">*</span></label>
<input class="form-input w-full" name="email" type="email" required />
</div>
<div class="col-12">
<label class="mb-2 block" for="subject">Subject</label>
<input
class="form-input w-full"
name="subject"
type="text"
/>
</div>
<div class="col-12">
<label class="mb-2 block" for="message">Message <span class="text-red-600">*</span></label>
<textarea class="form-textarea w-full" rows="4"></textarea>
</div>
<div class="col-12">
<button class="btn btn-primary mt-2">Submit Now</button>
</div>
</form>
</div>
</div>
</div>
</section>
</Base>

22
src/pages/index.astro Normal file
View File

@@ -0,0 +1,22 @@
---
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import Pagination from "@/layouts/components/Pagination.astro";
import Posts from "@/layouts/Posts.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { sortByDate } from "@/lib/utils/sortFunctions";
const posts = await getSinglePage("posts");
const sortedPosts = sortByDate(posts);
const totalPages = Math.ceil(posts.length / config.settings.pagination);
const currentPosts = sortedPosts.slice(0, config.settings.pagination);
---
<Base>
<section class="section">
<div class="container">
<Posts posts={currentPosts} className="mb-16" />
<Pagination currentPage={1} totalPages={totalPages} />
</div>
</section>
</Base>

View File

@@ -0,0 +1,41 @@
---
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import Pagination from "@/layouts/components/Pagination.astro";
import Posts from "@/layouts/Posts.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { sortByDate } from "@/lib/utils/sortFunctions";
export async function getStaticPaths() {
const posts = await getSinglePage("posts");
const totalPages = Math.ceil(posts.length / config.settings.pagination);
const paths = [];
for (let i = 1; i < totalPages; i++) {
paths.push({
params: {
slug: (i + 1).toString(),
},
});
}
return paths;
}
const { slug } = Astro.params;
const posts = await getSinglePage("posts");
const sortedPosts = sortByDate(posts);
const totalPages = Math.ceil(posts.length / config.settings.pagination);
const currentPage = slug && !isNaN(Number(slug)) ? Number(slug) : 1;
const indexOfLastPost = currentPage * config.settings.pagination;
const indexOfFirstPost = indexOfLastPost - config.settings.pagination;
const currentPosts = sortedPosts.slice(indexOfFirstPost, indexOfLastPost);
---
<Base>
<section class="section">
<div class="container">
<Posts className="mb-16" posts={currentPosts} />
<Pagination totalPages={totalPages} currentPage={currentPage} />
</div>
</section>
</Base>

23
src/pages/search.astro Normal file
View File

@@ -0,0 +1,23 @@
---
import Base from "@/layouts/Base.astro";
import SearchBar from "@/layouts/SearchBar";
import { getSinglePage } from "@/lib/contentParser.astro";
// Retrieve all articles
const posts = await getSinglePage("posts");
// List of items to search in
const searchList = posts.map((item: any) => ({
slug: item.slug,
data: item.data,
content: item.body,
}));
---
<Base title={`Search`}>
<section class="section">
<div class="container">
<SearchBar client:load searchList={searchList} />
</div>
</section>
</Base>

View File

@@ -0,0 +1,35 @@
---
import Base from "@/layouts/Base.astro";
import Posts from "@/layouts/Posts.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import taxonomyFilter from "@/lib/utils/taxonomyFilter";
import { humanize } from "@/lib/utils/textConverter";
export async function getStaticPaths() {
const tags = await getTaxonomy("posts", "tags");
return tags.map((tag) => {
return {
params: { tag },
};
});
}
const { tag } = Astro.params;
const posts = await getSinglePage("posts");
const filterByTags = taxonomyFilter(posts, "tags", tag);
const title = humanize(tag || "");
---
<Base title={title || "Tag"}>
<div class="section">
<div class="container">
<p class="text-center mb-4">Showing Posts From</p>
<h1 class="h2 mb-16 text-center text-primary">{title}</h1>
<Posts posts={filterByTags} fluid={false} />
</div>
</div>
</Base>

View File

@@ -0,0 +1,32 @@
---
import Base from "@/layouts/Base.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import { humanize } from "@/lib/utils/textConverter";
import { FaHashtag } from "react-icons/fa";
const tags = await getTaxonomy("posts", "tags");
---
<Base title={"Tags"}>
<section class="section">
<div class="container text-center">
<h1 class="h2 page-heading">Tags</h1>
<ul class="space-x-4">
{
tags.map((tag) => (
<li class="inline-block">
<a
href={`/tags/${tag}`}
class="rounded-lg bg-theme-light px-4 py-2 text-dark transition hover:bg-primary hover:text-white flex items-center group"
>
<FaHashtag className="mr-1 text-primary group-hover:text-white transition" />
<>{humanize(tag || "")}</>
</a>
</li>
))
}
</ul>
</div>
</section>
</Base>

46
src/styles/base.scss Normal file
View File

@@ -0,0 +1,46 @@
html {
@apply text-base;
}
body {
@apply bg-body font-primary font-normal leading-relaxed text-text;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-primary font-bold leading-tight text-dark;
}
h1,
.h1 {
@apply text-h1-sm md:text-h1;
}
h2,
.h2 {
@apply text-h2-sm md:text-h2;
}
h3,
.h3 {
@apply text-h3-sm md:text-h3;
}
h4,
.h4 {
@apply text-h4;
}
h5,
.h5 {
@apply text-h5;
}
h6,
.h6 {
@apply text-h6;
}

11
src/styles/buttons.scss Normal file
View File

@@ -0,0 +1,11 @@
.btn {
@apply inline-block rounded-lg border px-6 py-3 font-semibold transition;
}
.btn-primary {
@apply bg-primary text-white hover:text-white;
}
.btn-outline-primary {
@apply border-primary bg-transparent hover:bg-primary hover:text-white;
}

187
src/styles/components.scss Normal file
View File

@@ -0,0 +1,187 @@
// section style
.section {
@apply py-20;
}
// container
.container {
@apply max-w-[1000px] px-4 md:px-8;
}
// page heading
.page-heading {
@apply mb-20 text-center font-bold leading-10;
&:after {
@apply mx-auto mt-4 block h-1 w-10 rounded-full bg-primary content-[''];
}
}
// form style
.form-inputs * {
@apply mb-5 leading-10;
}
// image cover
.img-cover {
@apply leading-none;
span {
@apply h-full w-full;
}
img {
@apply object-cover;
}
}
// author-image
.author-image {
@apply mr-2 align-top;
img {
@apply max-h-[25px] max-w-[25px] rounded-full;
}
}
// social icon style
.social-icons {
@apply space-x-4;
li {
@apply inline-block;
a {
@apply block h-11 w-11 rounded-lg border border-light bg-transparent text-center text-white transition hover:border-primary hover:bg-primary;
svg {
@apply m-auto h-11 text-base;
}
}
}
}
.social-icons-simple {
@apply space-x-2;
li {
@apply inline-block;
a {
@apply block p-3 text-dark transition hover:text-primary;
svg {
@apply text-lg;
}
}
}
}
.social-share {
@apply space-x-1;
li {
@apply inline-block;
a {
@apply block p-3 transition transition duration-300 hover:text-primary;
}
}
}
// form style
.form-input,
.form-textarea {
@apply rounded-md border-border py-3 text-text focus:border-primary focus:ring-transparent;
}
// content style
.content {
@apply prose max-w-none prose-headings:font-bold prose-h1:mb-4 prose-h1:text-h1-sm prose-h2:mb-4 prose-h2:mt-4 prose-h2:text-h2-sm prose-h3:mt-4 prose-h3:text-h3-sm prose-h4:mt-4 prose-h5:mb-4 prose-h6:mb-6 prose-blockquote:rounded-lg prose-blockquote:border-primary prose-blockquote:bg-theme-light prose-blockquote:px-7 prose-blockquote:py-3 prose-blockquote:text-lg prose-blockquote:leading-8 prose-pre:px-6 prose-pre:py-5 md:prose-h1:text-h1 md:prose-h2:text-h2 md:prose-h3:text-h3;
}
// tab
.tab {
@apply overflow-hidden rounded-lg border border-border;
&-nav {
@apply flex border-b border-border bg-theme-light;
@apply m-0 #{!important};
@apply list-none #{!important};
&-item {
@apply cursor-pointer border-b-[3px] border-border py-2 text-lg text-dark opacity-80;
@apply my-0 #{!important};
@apply px-8 #{!important};
&.active {
@apply border-b-[3px] border-dark opacity-100;
}
}
}
&-content {
&-panel {
@apply p-8;
p {
@apply mb-0;
}
&.active {
@apply block;
}
}
}
}
// accordion
.accordion {
@apply mb-6 overflow-hidden rounded-lg border border-border bg-theme-light;
&-header {
@apply flex w-full cursor-pointer items-center justify-between px-8 py-4 text-lg text-dark;
}
&-icon {
@apply ml-auto h-[.8em] w-[.8em] rotate-[-90deg] transition-transform duration-200;
}
&-content {
@apply max-h-0 overflow-hidden px-8 py-0;
}
&.active {
.accordion-icon {
@apply rotate-0;
}
.accordion-content {
@apply max-h-screen;
}
}
}
// notice
.notice {
@apply mb-6 rounded-lg border px-8 py-6;
&-head {
@apply flex items-center;
svg {
@apply mr-3;
}
p {
@apply font-secondary text-xl font-semibold text-dark;
}
}
.notice-body {
@apply mt-3;
p {
@apply my-0;
}
}
&.note {
@apply text-[#1B83E2];
@apply border-current;
}
&.tip {
@apply text-[#40D294];
@apply border-current;
}
&.info {
@apply text-[#E3A72C];
@apply border-current;
}
&.warning {
@apply text-[#DB2C23];
@apply border-current;
}
}
// footer
footer p a {
@apply transition-all duration-200 hover:text-white;
}

17
src/styles/main.scss Normal file
View File

@@ -0,0 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
@import "base";
}
@layer components {
@import "components";
@import "navigation";
@import "buttons";
}
@layer utilities {
@import "utilities";
}

Some files were not shown because too many files have changed in this diff Show More