commit 56f6d0338585d1ce215139fc842abdaea28e7aca Author: Gary Kwok Date: Mon Jun 17 18:05:05 2024 +0800 v1 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..896e0d7 --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aea7f7c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..ae399b8 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD033": false, + "MD013": false +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..21b54aa --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": ["*.astro"], + "options": { + "parser": "astro" + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..30810fa --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["astro-build.astro-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..766f97d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.mdx": "markdown" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3616810 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..0bc02d2 --- /dev/null +++ b/Jenkinsfile @@ -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') + } + } + } + + + + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf799da --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f952a0e --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +

Bookworm Light Astro

+

Bookworm Light is a feature-rich, minimal, highly customizable, easy-to-use free Astro blog theme.

+

👀Demo | Page Speed (100%)🚀 +

+ +

+ + + + + + license + + code size + + + contributors +

+ +![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 + +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 ctrl/cmd+\`] + +- 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 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). It’s 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). + + +## 📄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). diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..d2c210c --- /dev/null +++ b/astro.config.mjs @@ -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, + }, +}); diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..809704f --- /dev/null +++ b/netlify.toml @@ -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" diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..43ead33 --- /dev/null +++ b/nginx/nginx.conf @@ -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; + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e4dd743 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b848eda --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,88 @@ +##### Optimize default expiration time - BEGIN + + + ## 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" + +##### Optimize default expiration time - END + +##### 1 Month for most static resources + + Header set Cache-Control "max-age=2592000, public" + + +##### Enable gzip compression for resources + + 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.* + + +##### Or, compress certain file types by extension: + + SetOutputFilter DEFLATE + + +##### Set Header Vary: Accept-Encoding + + + Header append Vary: Accept-Encoding + + \ No newline at end of file diff --git a/public/images/author.png b/public/images/author.png new file mode 100644 index 0000000..68f565b Binary files /dev/null and b/public/images/author.png differ diff --git a/public/images/authors/john-doe.jpg b/public/images/authors/john-doe.jpg new file mode 100644 index 0000000..7957aa3 Binary files /dev/null and b/public/images/authors/john-doe.jpg differ diff --git a/public/images/authors/mark-dinn.jpg b/public/images/authors/mark-dinn.jpg new file mode 100644 index 0000000..05452a4 Binary files /dev/null and b/public/images/authors/mark-dinn.jpg differ diff --git a/public/images/favicon.png b/public/images/favicon.png new file mode 100644 index 0000000..2cbb178 Binary files /dev/null and b/public/images/favicon.png differ diff --git a/public/images/image-placeholder.png b/public/images/image-placeholder.png new file mode 100644 index 0000000..a61a0c0 Binary files /dev/null and b/public/images/image-placeholder.png differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..b363c6c Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/posts/01.jpg b/public/images/posts/01.jpg new file mode 100644 index 0000000..d732b8e Binary files /dev/null and b/public/images/posts/01.jpg differ diff --git a/public/images/posts/02.jpg b/public/images/posts/02.jpg new file mode 100644 index 0000000..4e6c5b0 Binary files /dev/null and b/public/images/posts/02.jpg differ diff --git a/public/images/posts/03.jpg b/public/images/posts/03.jpg new file mode 100644 index 0000000..c7e7b8f Binary files /dev/null and b/public/images/posts/03.jpg differ diff --git a/public/images/posts/04.jpg b/public/images/posts/04.jpg new file mode 100644 index 0000000..543b24c Binary files /dev/null and b/public/images/posts/04.jpg differ diff --git a/public/images/posts/05.jpg b/public/images/posts/05.jpg new file mode 100644 index 0000000..7c1ae8f Binary files /dev/null and b/public/images/posts/05.jpg differ diff --git a/public/images/posts/06.jpg b/public/images/posts/06.jpg new file mode 100644 index 0000000..83ce735 Binary files /dev/null and b/public/images/posts/06.jpg differ diff --git a/public/images/posts/07.jpg b/public/images/posts/07.jpg new file mode 100644 index 0000000..f3b6961 Binary files /dev/null and b/public/images/posts/07.jpg differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..9ea0e6f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Disallow: /api/* \ No newline at end of file diff --git a/src/config/config.json b/src/config/config.json new file mode 100644 index 0000000..801a8b9 --- /dev/null +++ b/src/config/config.json @@ -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": "" + } +} diff --git a/src/config/menu.json b/src/config/menu.json new file mode 100644 index 0000000..4b24569 --- /dev/null +++ b/src/config/menu.json @@ -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" + } + ] +} diff --git a/src/config/social.json b/src/config/social.json new file mode 100644 index 0000000..10ff832 --- /dev/null +++ b/src/config/social.json @@ -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": "" +} diff --git a/src/config/theme.json b/src/config/theme.json new file mode 100644 index 0000000..90ff3a6 --- /dev/null +++ b/src/config/theme.json @@ -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" + } + } +} diff --git a/src/content/about/index.md b/src/content/about/index.md new file mode 100644 index 0000000..f968ce3 --- /dev/null +++ b/src/content/about/index.md @@ -0,0 +1,23 @@ +--- +title: "I’m 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. diff --git a/src/content/authors/-index.md b/src/content/authors/-index.md new file mode 100644 index 0000000..62eae44 --- /dev/null +++ b/src/content/authors/-index.md @@ -0,0 +1,3 @@ +--- +title: "Authors" +--- diff --git a/src/content/authors/john-doe.md b/src/content/authors/john-doe.md new file mode 100644 index 0000000..af07fd0 --- /dev/null +++ b/src/content/authors/john-doe.md @@ -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. diff --git a/src/content/authors/mark-dinn.md b/src/content/authors/mark-dinn.md new file mode 100644 index 0000000..3717bd3 --- /dev/null +++ b/src/content/authors/mark-dinn.md @@ -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. diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..d4d8be6 --- /dev/null +++ b/src/content/config.ts @@ -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, +}; diff --git a/src/content/pages/404.md b/src/content/pages/404.md new file mode 100644 index 0000000..1915e40 --- /dev/null +++ b/src/content/pages/404.md @@ -0,0 +1,5 @@ +--- +title: "Error 404" +--- + +## Page Not Found diff --git a/src/content/pages/contact.md b/src/content/pages/contact.md new file mode 100644 index 0000000..7abef48 --- /dev/null +++ b/src/content/pages/contact.md @@ -0,0 +1,4 @@ +--- +title: "Contact" +draft: false +--- diff --git a/src/content/pages/elements.mdx b/src/content/pages/elements.mdx new file mode 100644 index 0000000..489c5ed --- /dev/null +++ b/src/content/pages/elements.mdx @@ -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 you’re 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 you’re 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 particular + +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 + +``` + +--- + +#### 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 + + +
{children}
+ + ); +}; + +export default Accordion; diff --git a/src/layouts/shortcodes/Button.tsx b/src/layouts/shortcodes/Button.tsx new file mode 100644 index 0000000..e601af9 --- /dev/null +++ b/src/layouts/shortcodes/Button.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +const Button = ({ + label, + link, + style, + rel, +}: { + label: string; + link: string; + style?: string; + rel?: string; +}) => { + return ( + + {label} + + ); +}; + +export default Button; diff --git a/src/layouts/shortcodes/Notice.tsx b/src/layouts/shortcodes/Notice.tsx new file mode 100644 index 0000000..48c4364 --- /dev/null +++ b/src/layouts/shortcodes/Notice.tsx @@ -0,0 +1,85 @@ +import { humanize } from "@/lib/utils/textConverter"; +import React from "react"; + +function Notice({ + type, + children, +}: { + type: string; + children: React.ReactNode; +}) { + return ( +
+
+ {type === "tip" ? ( + + + + ) : type === "info" ? ( + + + + + ) : type === "warning" ? ( + + + + ) : ( + + + + )} +

{humanize(type)}

+
+
{children}
+
+ ); +} + +export default Notice; diff --git a/src/layouts/shortcodes/Tab.tsx b/src/layouts/shortcodes/Tab.tsx new file mode 100644 index 0000000..051f6ce --- /dev/null +++ b/src/layouts/shortcodes/Tab.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +function Tab({ name, children }: { name: string; children: React.ReactNode }) { + return
{children}
; +} + +export default Tab; diff --git a/src/layouts/shortcodes/Tabs.tsx b/src/layouts/shortcodes/Tabs.tsx new file mode 100644 index 0000000..59aa44f --- /dev/null +++ b/src/layouts/shortcodes/Tabs.tsx @@ -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(0); + const [defaultFocus, setDefaultFocus] = useState(false); + + const tabRefs: React.RefObject = useRef([]); + useEffect(() => { + if (defaultFocus) { + //@ts-ignore + tabRefs.current[active]?.focus(); + } else { + setDefaultFocus(true); + } + }, [active]); + + const tabLinks = Array.from( + children.props.value.matchAll( + /]*>(.*?)<\/div>/gs, + ), + (match: RegExpMatchArray) => ({ name: match[1], children: match[0] }), + ); + + const handleKeyDown = ( + event: React.KeyboardEvent, + 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 ( +
+
    + {tabLinks.map( + (item: { name: string; children: string }, index: number) => ( +
  • handleKeyDown(event, index)} + onClick={() => setActive(index)} + //@ts-ignore + ref={(ref) => (tabRefs.current[index] = ref)} + > + {item.name} +
  • + ), + )} +
+ {tabLinks.map((item: { name: string; children: string }, i: number) => ( +
+ ))} +
+ ); +}; + +export default Tabs; diff --git a/src/layouts/shortcodes/Video.tsx b/src/layouts/shortcodes/Video.tsx new file mode 100644 index 0000000..3c22504 --- /dev/null +++ b/src/layouts/shortcodes/Video.tsx @@ -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 ( + + ); +} + +export default Video; diff --git a/src/layouts/shortcodes/Youtube.tsx b/src/layouts/shortcodes/Youtube.tsx new file mode 100644 index 0000000..74eb4d1 --- /dev/null +++ b/src/layouts/shortcodes/Youtube.tsx @@ -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 ( + + ); +}; + +export default Youtube; diff --git a/src/lib/contentParser.astro b/src/lib/contentParser.astro new file mode 100644 index 0000000..a6d1013 --- /dev/null +++ b/src/lib/contentParser.astro @@ -0,0 +1,16 @@ +--- +import { + getCollection, + type CollectionEntry, + type CollectionKey, +} from "astro:content"; + +export const getSinglePage = async ( + collectionName: C, +): Promise[]> => { + const allPages = await getCollection(collectionName); + const removeIndex = allPages.filter((data) => data.id.match(/^(?!-)/)); + const removeDrafts = removeIndex.filter((data) => !data.data.draft); + return removeDrafts; +}; +--- diff --git a/src/lib/taxonomyParser.astro b/src/lib/taxonomyParser.astro new file mode 100644 index 0000000..88cc8d3 --- /dev/null +++ b/src/lib/taxonomyParser.astro @@ -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; +}; +--- diff --git a/src/lib/utils/dateFormat.ts b/src/lib/utils/dateFormat.ts new file mode 100644 index 0000000..9b960d5 --- /dev/null +++ b/src/lib/utils/dateFormat.ts @@ -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; diff --git a/src/lib/utils/readingTime.ts b/src/lib/utils/readingTime.ts new file mode 100644 index 0000000..ebd8fcc --- /dev/null +++ b/src/lib/utils/readingTime.ts @@ -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(" 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; diff --git a/src/lib/utils/similarItems.ts b/src/lib/utils/similarItems.ts new file mode 100644 index 0000000..d402ba0 --- /dev/null +++ b/src/lib/utils/similarItems.ts @@ -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; diff --git a/src/lib/utils/sortFunctions.ts b/src/lib/utils/sortFunctions.ts new file mode 100644 index 0000000..6ee2fee --- /dev/null +++ b/src/lib/utils/sortFunctions.ts @@ -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; +}; diff --git a/src/lib/utils/taxonomyFilter.ts b/src/lib/utils/taxonomyFilter.ts new file mode 100644 index 0000000..1341721 --- /dev/null +++ b/src/lib/utils/taxonomyFilter.ts @@ -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; diff --git a/src/lib/utils/textConverter.ts b/src/lib/utils/textConverter.ts new file mode 100644 index 0000000..257ea06 --- /dev/null +++ b/src/lib/utils/textConverter.ts @@ -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 } = { + " ": " ", + "<": "<", + ">": ">", + "&": "&", + """: '"', + "'": "'", + }; + let htmlWithoutEntities: string = htmlWithEntities.replace( + /(&|<|>|"|')/g, + (entity: string): string => { + return entityList[entity]; + } + ); + return htmlWithoutEntities; +}; diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..bc2c787 --- /dev/null +++ b/src/pages/404.astro @@ -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(); +--- + + +
+
+
+
+

+ +

+
+
+
+ diff --git a/src/pages/[regular].astro b/src/pages/[regular].astro new file mode 100644 index 0000000..8228608 --- /dev/null +++ b/src/pages/[regular].astro @@ -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; +--- + + + { + postsSlug.includes(page.slug) ? ( + + ) : ( + + ) + } + diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..5c91df5 --- /dev/null +++ b/src/pages/about.astro @@ -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; +--- + + +
+
+
+
+ { + image && ( +
+ {title} +
+ ) + } +
+
+

+ +
+ +
+ Get In Touch +

+
+
+
+ +
+
+

{what_i_do.title}

+
+ { + what_i_do.items.map((item: any) => ( +
+ +

{item.title}

+

{item.description}

+
+ )) + } +
+
+
+ diff --git a/src/pages/authors/[single].astro b/src/pages/authors/[single].astro new file mode 100644 index 0000000..45e264a --- /dev/null +++ b/src/pages/authors/[single].astro @@ -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)); +}); +--- + + + + + { + currentPosts.length > 0 && ( +
+
+

Recent Posts

+
+ {currentPosts.map((post: any, i: number) => ( +
+ {post.data.image && ( + + {post.data.title} + + )} + +

+ + {post.data.title} + +

+
+ ))} +
+
+
+ ) + } + diff --git a/src/pages/authors/index.astro b/src/pages/authors/index.astro new file mode 100644 index 0000000..a036012 --- /dev/null +++ b/src/pages/authors/index.astro @@ -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); +--- + + +
+
+

+ + +

+
+ diff --git a/src/pages/authors/page/[slug].astro b/src/pages/authors/page/[slug].astro new file mode 100644 index 0000000..dd4a810 --- /dev/null +++ b/src/pages/authors/page/[slug].astro @@ -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); +--- + + +
+
+

+ + +

+
+ diff --git a/src/pages/categories/[category].astro b/src/pages/categories/[category].astro new file mode 100644 index 0000000..d4aa825 --- /dev/null +++ b/src/pages/categories/[category].astro @@ -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 || ""); +--- + + +
+
+

Showing Posts From

+

{title}

+ +
+
+ diff --git a/src/pages/categories/index.astro b/src/pages/categories/index.astro new file mode 100644 index 0000000..a453bbb --- /dev/null +++ b/src/pages/categories/index.astro @@ -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"; +--- + + +
+
+

Categories

+ +
+
+ diff --git a/src/pages/contact.astro b/src/pages/contact.astro new file mode 100644 index 0000000..345d5d5 --- /dev/null +++ b/src/pages/contact.astro @@ -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; +--- + + +
+
+

+
+
+

Contact Info

+
    +
  • +
    + +

    Address

    +
    +

    +

  • +
  • +
    + +

    Email

    +
    +

    +

  • +
  • +
    + +

    Phone

    +
    +

    +

  • +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+

+
+ diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..eb7fa04 --- /dev/null +++ b/src/pages/index.astro @@ -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); +--- + + +
+
+ + +
+
+ diff --git a/src/pages/page/[slug].astro b/src/pages/page/[slug].astro new file mode 100644 index 0000000..717faf2 --- /dev/null +++ b/src/pages/page/[slug].astro @@ -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); +--- + + +
+
+ + +
+
+ diff --git a/src/pages/search.astro b/src/pages/search.astro new file mode 100644 index 0000000..cb71aaa --- /dev/null +++ b/src/pages/search.astro @@ -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, +})); +--- + + +
+
+ +
+
+ diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro new file mode 100644 index 0000000..a5434e5 --- /dev/null +++ b/src/pages/tags/[tag].astro @@ -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 || ""); +--- + + +
+
+

Showing Posts From

+

{title}

+ +
+
+ diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro new file mode 100644 index 0000000..5c7f210 --- /dev/null +++ b/src/pages/tags/index.astro @@ -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"); +--- + + +
+
+

Tags

+ +
+
+ diff --git a/src/styles/base.scss b/src/styles/base.scss new file mode 100644 index 0000000..f454095 --- /dev/null +++ b/src/styles/base.scss @@ -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; +} diff --git a/src/styles/buttons.scss b/src/styles/buttons.scss new file mode 100644 index 0000000..44c1396 --- /dev/null +++ b/src/styles/buttons.scss @@ -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; +} diff --git a/src/styles/components.scss b/src/styles/components.scss new file mode 100644 index 0000000..2f9d91f --- /dev/null +++ b/src/styles/components.scss @@ -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; +} diff --git a/src/styles/main.scss b/src/styles/main.scss new file mode 100644 index 0000000..1875eba --- /dev/null +++ b/src/styles/main.scss @@ -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"; +} diff --git a/src/styles/navigation.scss b/src/styles/navigation.scss new file mode 100644 index 0000000..b6b5138 --- /dev/null +++ b/src/styles/navigation.scss @@ -0,0 +1,59 @@ +// navbar toggler +input#nav-toggle:checked ~ label#show-button { + @apply hidden; +} + +input#nav-toggle:checked ~ label#hide-button { + @apply flex md:hidden; +} + +input#nav-toggle:checked ~ #nav-menu { + @apply block md:flex; +} + +// navbar items +.navbar { + @apply relative flex max-w-[1260px] flex-wrap items-center justify-between; +} + +.navbar-brand img { + @apply h-auto max-w-[170px] sm:h-auto sm:max-w-full #{!important}; +} + +.navbar-nav { + @apply text-center md:text-left; +} + +.nav-item { + @apply mx-1; +} + +.nav-link { + @apply p-3 text-lg font-semibold text-dark transition hover:text-primary md:p-4; +} + +.nav-dropdown-list { + @apply z-10 rounded-lg bg-white px-6 py-3 shadow transition; +} + +.nav-dropdown-item { + @apply mb-1; +} + +.nav-dropdown-link { + @apply block min-w-[150px] py-1 text-[17px] font-semibold text-dark transition hover:text-primary; +} + +// search style +.search-modal { + @apply invisible absolute top-0 left-0 right-0 z-10 h-10 bg-white opacity-0 transition md:h-full; + .form-input { + @apply h-full w-full border-0 text-lg; + } + .search-close { + @apply absolute top-1/2 right-2 -translate-y-1/2 p-3 text-h4; + } + &.open { + @apply visible opacity-100; + } +} diff --git a/src/styles/utilities.scss b/src/styles/utilities.scss new file mode 100644 index 0000000..5bed0e9 --- /dev/null +++ b/src/styles/utilities.scss @@ -0,0 +1,12 @@ +b, +strong { + @apply font-semibold; +} + +img { + @apply inline-block; +} + +.shadow { + box-shadow: 0 10px 30px rgb(22 28 45 / 10%); +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..ed527b6 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,35 @@ +export interface TAuthor { + id: string; + slug: string; + body: string; + collection: string; + data: { + title: string; + image: string; + description: string; + social: { + facebook: string; + twitter: string; + instagram: string; + } + }; + render: () => Promise<{ Content: any }>; +} + +export interface TPost { + id: string; + slug: string; + body: string; + collection: string; + data: { + title: string; + description: string; + date: any; + image: string; + authors: string[]; + categories: string[]; + tags: string[]; + draft: boolean; + }; + render: () => Promise<{ Content: any }>; +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..5ba2781 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,84 @@ +const theme = require("./src/config/theme.json"); + +let font_base = Number(theme.fonts.font_size.base.replace("px", "")); +let font_scale = Number(theme.fonts.font_size.scale); +let h6 = font_base / font_base; +let h5 = h6 * font_scale; +let h4 = h5 * font_scale; +let h3 = h4 * font_scale; +let h2 = h3 * font_scale; +let h1 = h2 * font_scale; +let fontPrimary, fontPrimaryType, fontSecondary, fontSecondaryType; +if (theme.fonts.font_family.primary) { + fontPrimary = theme.fonts.font_family.primary + .replace(/\+/g, " ") + .replace(/:[ital,]*[ital@]*[wght@]*[0-9,;]+/gi, ""); + fontPrimaryType = theme.fonts.font_family.primary_type; +} +if (theme.fonts.font_family.secondary) { + fontSecondary = theme.fonts.font_family.secondary + .replace(/\+/g, " ") + .replace(/:[ital,]*[ital@]*[wght@]*[0-9,;]+/gi, ""); + fontSecondaryType = theme.fonts.font_family.secondary_type; +} + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], + theme: { + screens: { + sm: "540px", + md: "768px", + lg: "1024px", + xl: "1280px", + "2xl": "1536px", + }, + container: { + center: true, + padding: "2rem", + }, + extend: { + colors: { + text: theme.colors.default.text_color.default, + light: theme.colors.default.text_color.light, + dark: theme.colors.default.text_color.dark, + primary: theme.colors.default.theme_color.primary, + secondary: theme.colors.default.theme_color.secondary, + body: theme.colors.default.theme_color.body, + border: theme.colors.default.theme_color.border, + "theme-light": theme.colors.default.theme_color.theme_light, + "theme-dark": theme.colors.default.theme_color.theme_dark, + }, + fontSize: { + base: font_base + "px", + h1: h1 + "rem", + "h1-sm": h1 * 0.8 + "rem", + h2: h2 + "rem", + "h2-sm": h2 * 0.8 + "rem", + h3: h3 + "rem", + "h3-sm": h3 * 0.8 + "rem", + h4: h4 + "rem", + h5: h5 + "rem", + h6: h6 + "rem", + }, + fontFamily: { + primary: [fontPrimary, fontPrimaryType], + secondary: [fontSecondary, fontSecondaryType], + }, + }, + }, + plugins: [ + require("@tailwindcss/typography"), + require("@tailwindcss/forms"), + require("tailwind-bootstrap-grid")({ + generateContainer: false, + gridGutters: { + 1: "0.5rem", + 2: "0.75rem", + 3: "1.25rem", + 4: "2rem", + 5: "3.5rem", + }, + }), + ], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..18d9566 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "target": "es6", + "allowJs": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "jsx": "react", + "isolatedModules": true, + "incremental": true, + "allowSyntheticDefaultImports": true, + "paths": { + "@/components/*": ["./src/layouts/components/*"], + "@/shortcodes/*": ["./src/layouts/shortcodes/*"], + "@/helpers/*": ["./src/layouts/helpers/*"], + "@/partials/*": ["./src/layouts/partials/*"], + "@/*": ["./src/*"] + } + }, + "include": ["**/*.ts", "**/*.tsx", "**/*.astro"], + "exclude": ["node_modules"] +}