150600
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
.vscode
|
||||
18
LICENSE
@@ -1,18 +0,0 @@
|
||||
LICENSE
|
||||
=======
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted, provided
|
||||
that the original copyright notices appear in all copies and that both
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation, and that the name of the author not be used in advertising
|
||||
or publicity pertaining to distribution of the software without specific
|
||||
prior written permission.
|
||||
|
||||
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.
|
||||
40
README.md
@@ -1,2 +1,40 @@
|
||||
# stusla.ru
|
||||
# Gblog is an open-source, simple, and beautiful blog built with Astro.
|
||||
|
||||

|
||||
|
||||
Gblog is a blog template designed for those who are unable or unwilling to write frontend code. All you need to do is find an interesting Tailwind CSS template from elsewhere and paste it into Gblog, and it will function seamlessly. Additionally, you can customize your own blog without to write any JavaScript code.
|
||||
|
||||
**[View Live Demo](https://godruoyi.com)**
|
||||
|
||||
## Features
|
||||
|
||||
- 🐈 Simple And Beautiful
|
||||
- 🖥️️ Responsive And Light/Dark mode
|
||||
- 🐛 SiteMap & RSS Feed
|
||||
- 🐝 Category and Timeline Support
|
||||
- 🍋 Google Analytics & Google Structured Data
|
||||
- 🐜 SEO and Responsiveness
|
||||
- 🪲 Markdown And MDX
|
||||
- 🏂🏾 Page Compression & Image Optimization
|
||||
|
||||
## Make Your Own
|
||||
|
||||
1. Clone the `astro` branch of this repository `git clone -b astro git@github.com:godruoyi/gblog.git`
|
||||
2. Execute `pnpm install` to install dependencies.
|
||||
3. Modify the `src/config.ts` file to what you want.
|
||||
4. Execute `pnpm run dev`: Starts a local development server with hot reloading enabled.
|
||||
|
||||
### Deployment
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgodruoyi%2Fgblog&project-name=my-blog&repository-name=my-blog)
|
||||
|
||||
|
||||
See [Deploy your Astro Site](https://docs.astro.build/en/guides/deploy/) get more info.
|
||||
|
||||
## Thanks
|
||||
|
||||
Thanks https://github.com/mearashadowfax/ScrewFast, The majority of the code for this project comes from ScrewFast.
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the MIT License. Please read the [LICENSE](https://github.com/godruoyi/gblog/blob/astro/LICENSE) file for more details.
|
||||
|
||||
43
astro.config.mjs
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
import mdx from '@astrojs/mdx'
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
import react from '@astrojs/react'
|
||||
import sitemap from '@astrojs/sitemap'
|
||||
import partytown from '@astrojs/partytown'
|
||||
import { SITE } from './src/config.ts'
|
||||
import { remarkReadingTime } from './src/support/time.ts'
|
||||
|
||||
export default defineConfig({
|
||||
site: SITE.url,
|
||||
image: {},
|
||||
integrations: [
|
||||
mdx(),
|
||||
sitemap(),
|
||||
tailwind(),
|
||||
react(),
|
||||
partytown(),
|
||||
(await import('@playform/compress')).default({
|
||||
CSS: true,
|
||||
HTML: true,
|
||||
Image: false, // too slow when deploy to production,
|
||||
JavaScript: true,
|
||||
SVG: true,
|
||||
Logger: 2,
|
||||
}),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [remarkReadingTime],
|
||||
shikiConfig: {
|
||||
themes: {
|
||||
light: 'material-theme-lighter',
|
||||
dark: 'one-dark-pro',
|
||||
},
|
||||
wrap: false,
|
||||
},
|
||||
},
|
||||
output: 'static',
|
||||
// experimental: {
|
||||
// clientPrerender: true,
|
||||
// directRenderScript: true,
|
||||
// },
|
||||
})
|
||||
22
eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu({
|
||||
stylistic: {
|
||||
indent: 4, // 4, or 'tab'
|
||||
quotes: 'single', // or 'double'
|
||||
},
|
||||
|
||||
react: true,
|
||||
typescript: true,
|
||||
vue: true,
|
||||
astro: true,
|
||||
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'curly': ['error', 'all'],
|
||||
'node/prefer-global/process': 'off',
|
||||
},
|
||||
|
||||
jsonc: false,
|
||||
yaml: false,
|
||||
})
|
||||
18240
package-lock.json
generated
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "faithful-filament",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev --port 8005",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^1.1.0",
|
||||
"@astrojs/partytown": "^2.1.0",
|
||||
"@astrojs/react": "^3.3.0",
|
||||
"@astrojs/rss": "^3.0.0",
|
||||
"@astrojs/sitemap": "^3.0.0",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@playform/compress": "^0.0.4",
|
||||
"astro": "^3.1.0",
|
||||
"astro-compressor": "^0.4.1",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"sharp": "^0.33.4",
|
||||
"sharp-ico": "^0.1.5",
|
||||
"tailwindcss": "^3.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.13.3",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-astro": "^0.34.0",
|
||||
"eslint-plugin-format": "^0.1.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"mysql2": "^3.9.7",
|
||||
"preline": "^2.1.0",
|
||||
"timeago.js": "^4.0.2"
|
||||
}
|
||||
}
|
||||
9441
pnpm-lock.yaml
generated
Normal file
1
public/vendor/lenis/lenis1.0.42.min.js
vendored
Normal file
23
public/vendor/preline/collapse2.1.0.min.js
vendored
Normal file
0
scripts/change_post_banner.js
Normal file
173
scripts/db_to_md.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Load all posts from my blog database and write them to Markdown file
|
||||
*
|
||||
* usage:
|
||||
*
|
||||
* node db_to_md.js db_username db_password
|
||||
*/
|
||||
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as fs from 'node:fs'
|
||||
import mysql from 'mysql2/promise'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
class BlogToMarkdown {
|
||||
databaseName = 'godruoyi'
|
||||
|
||||
host = '127.0.0.1'
|
||||
|
||||
port = 13306
|
||||
|
||||
constructor() {
|
||||
const { username, password } = this.parseProcessArgs()
|
||||
|
||||
this.username = username
|
||||
this.password = password
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.connection = await this.connectMySQL(this.username, this.password)
|
||||
|
||||
const categoryMap = await this.fetchCategories()
|
||||
const posts = await this.fetchPosts()
|
||||
|
||||
const formatedPosts = this.formatPosts(categoryMap, posts)
|
||||
|
||||
for (const p of formatedPosts) {
|
||||
// this.writePostToMarkdown(p)
|
||||
await this.savePostBanner(p)
|
||||
}
|
||||
|
||||
console.log('All done')
|
||||
}
|
||||
|
||||
async savePostBanner(post) {
|
||||
const filePath = this.postBannerFilePath(post)
|
||||
console.log('savePostBanner', filePath)
|
||||
|
||||
const response = await fetch(post.banner)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('download image failed')
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
fs.writeFileSync(filePath, Buffer.from(arrayBuffer))
|
||||
}
|
||||
|
||||
async fetchPosts() {
|
||||
const query = 'SELECT * FROM posts ORDER BY id ASC'
|
||||
|
||||
const [rows] = await this.connection.execute(query)
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
async fetchCategories() {
|
||||
const query = 'SELECT * FROM categories ORDER BY id ASC'
|
||||
|
||||
const [rows] = await this.connection.execute(query)
|
||||
|
||||
return rows.reduce((map, obj) => map.set(obj.id, obj), new Map())
|
||||
}
|
||||
|
||||
writePostToMarkdown(post) {
|
||||
console.log(`saving post to markdown: ${post.title}`)
|
||||
|
||||
const file = this.postToMarkdownFilePath(post)
|
||||
const content = this.convertPostToMarkdown(post)
|
||||
|
||||
fs.writeFileSync(file, content, 'utf-8')
|
||||
}
|
||||
|
||||
postToMarkdownFilePath(p) {
|
||||
return join(__dirname, `./../src/content/posts/${p.slug}.md`)
|
||||
}
|
||||
|
||||
postBannerFilePath(p) {
|
||||
const name = new URL(p.banner).pathname.split('/').pop()
|
||||
return join(__dirname, `./../src/images/posts/${name}`)
|
||||
}
|
||||
|
||||
convertPostToMarkdown(post) {
|
||||
return `---
|
||||
title: "${post.title}"
|
||||
description: "${post.description}"
|
||||
pubDate: "${this.formatDate(post.pubDate)}"
|
||||
category: "${post.category.slug}"
|
||||
cardImage: "${post.banner}"
|
||||
tags: ["${post.category.slug}"]
|
||||
oldViewCount: ${post.viewCount}
|
||||
oldKeywords: ["${post.keyword}"]
|
||||
---
|
||||
|
||||
${post.content}
|
||||
`
|
||||
}
|
||||
|
||||
formatPosts(categoryMap, posts) {
|
||||
return posts.map((p) => {
|
||||
return {
|
||||
title: p.title,
|
||||
description: p.excerpt,
|
||||
pubDate: p.created_at,
|
||||
banner: p.banner,
|
||||
category_id: p.category_id,
|
||||
category: categoryMap.get(p.category_id),
|
||||
slug: p.slug,
|
||||
viewCount: p.view_count,
|
||||
content: p.content,
|
||||
id: p.id,
|
||||
keyword: p.keyword,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async connectMySQL(username, password) {
|
||||
return mysql.createConnection({
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
user: username,
|
||||
password,
|
||||
database: this.databaseName,
|
||||
})
|
||||
}
|
||||
|
||||
closeConnection() {
|
||||
if (this.connection) {
|
||||
this.connection.close()
|
||||
}
|
||||
}
|
||||
|
||||
parseProcessArgs() {
|
||||
const username = process.argv[2]
|
||||
const password = process.argv[3]
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error('Missing username or password')
|
||||
}
|
||||
|
||||
return { username, password }
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
const newDate = new Date(date.getTime() + 8 * 60 * 60 * 1000) // Date object uses milliseconds
|
||||
|
||||
const yyyy = newDate.getUTCFullYear()
|
||||
const mm = String(newDate.getUTCMonth() + 1).padStart(2, '0') // Months are zero based
|
||||
const dd = String(newDate.getUTCDate()).padStart(2, '0')
|
||||
|
||||
const hh = String(newDate.getUTCHours()).padStart(2, '0')
|
||||
const mi = String(newDate.getUTCMinutes()).padStart(2, '0')
|
||||
const ss = String(newDate.getUTCSeconds()).padStart(2, '0')
|
||||
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
||||
}
|
||||
}
|
||||
|
||||
const x = new BlogToMarkdown()
|
||||
await x.run()
|
||||
await process.exit(0)
|
||||
82
src/components/BaseHead.astro
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
import faviconSvgSrc from '@images/icon.svg'
|
||||
import { getImage } from 'astro:assets'
|
||||
import faviconSrc from '@images/icon.png'
|
||||
import socialSrc from '@images/social-home.png'
|
||||
import { SEO, SITE } from '../config'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
description: string
|
||||
ogImage?: string
|
||||
ogTitle?: string
|
||||
ogDescription?: string
|
||||
structuredData?: object
|
||||
}
|
||||
|
||||
const canonicalURL = Astro.url.href
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
ogImage = socialSrc,
|
||||
ogTitle = title,
|
||||
ogDescription = description,
|
||||
structuredData = SEO.structuredData,
|
||||
} = Astro.props
|
||||
|
||||
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' })
|
||||
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' })
|
||||
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 })
|
||||
const socialImage = Astro.url.origin + socialImageRes.src
|
||||
---
|
||||
|
||||
<!-- Inject structured data https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data -->
|
||||
{
|
||||
structuredData && (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify(structuredData)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="web_author" content={SITE.author} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#facc15" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:site_name" content={SITE.title} />
|
||||
<meta property="og:description" content={ogDescription} />
|
||||
<meta property="og:image" content={socialImage} />
|
||||
<meta content="1200" property="og:image:width" />
|
||||
<meta content="600" property="og:image:height" />
|
||||
<meta content="image/png" property="og:image:type" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:domain" content={Astro.url} />
|
||||
<meta property="twitter:title" content={ogTitle} />
|
||||
<meta property="twitter:description" content={ogDescription} />
|
||||
<meta property="twitter:image" content={socialImage} />
|
||||
|
||||
<!-- Links -->
|
||||
<link href={canonicalURL} rel="canonical" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<!--<link href="/manifest.json" rel="manifest" />-->
|
||||
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
||||
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
|
||||
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
|
||||
<link href={appleTouchIcon.src} rel="shortcut icon" />
|
||||
56
src/components/Footer.astro
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
import BrandLogo from '@components/logos/BrandLogo.astro'
|
||||
import { Image } from 'astro:assets'
|
||||
import ponyo from 'images/footer-ponyo.png'
|
||||
import { FooterLinks, SITE } from '../config'
|
||||
---
|
||||
|
||||
<footer class="w-full bg-neutral-300 dark:bg-neutral-900">
|
||||
<div class="mx-auto w-full max-w-[85rem] px-4 py-10 sm:px-6 lg:px-16 lg:pt-20 2xl:max-w-screen-2xl">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-6 md:grid-cols-5">
|
||||
<div class="col-span-full flex justify-center sm:grid sm:col-span-1">
|
||||
<!--BrandLogo class="h-auto w-28 rounded-full" /-->
|
||||
</div>
|
||||
{
|
||||
FooterLinks.map(section => (
|
||||
<div class="col-span-1 text-center">
|
||||
<h3 class="font-bold text-neutral-800 dark:text-neutral-200">
|
||||
{section.section}
|
||||
</h3>
|
||||
<ul class="mt-3 grid space-y-3">
|
||||
{section.links.map((link, _index) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.url}
|
||||
class="inline-flex gap-x-2 rounded-lg text-neutral-600 outline-none ring-zinc-500 transition duration-300 hover:text-neutral-500 focus-visible:ring dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-neutral-300 dark:focus:outline-none"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<div class="col-span-2 flex justify-center mt-10 md:mt-0">
|
||||
<div class="max-w-[320px] max-h-[320px] -mt-16 scale-75 hidden md:block">
|
||||
<!--Image
|
||||
src={ponyo}
|
||||
alt={SITE.title}
|
||||
class="h-full w-full object-cover object-center"
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
/-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center text-center justify-center">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
© <span id="current-year">2024</span> {SITE.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
17
src/components/FormattedDate.astro
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
interface Props {
|
||||
date: Date
|
||||
}
|
||||
|
||||
const { date } = Astro.props;
|
||||
---
|
||||
|
||||
<time datetime={date.toISOString()}>
|
||||
{
|
||||
date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
</time>
|
||||
184
src/components/Header.astro
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
import { NavigationLinks } from '../config'
|
||||
import NavLink from './ui/NavLink.astro'
|
||||
import BrandLogo from './logos/BrandLogo.astro'
|
||||
import Theme from './logos/Theme.astro';
|
||||
---
|
||||
|
||||
<header class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm md:flex-nowrap md:justify-start">
|
||||
<nav
|
||||
class="relative mx-2 w-full rounded-[36px] border border-neutral-100 bg-neutral-100 px-4 py-3 backdrop-blur-md dark:border-neutral-700/40 dark:bg-neutral-800/80 dark:backdrop-blur-md md:flex md:items-center md:justify-between md:px-6 lg:px-8 xl:mx-auto"
|
||||
aria-label="Global"
|
||||
>
|
||||
<div class="flex items-center justify-between ">
|
||||
<a
|
||||
class="h-[42px] flex-none rounded-lg text-xl font-bold outline-none ring-zinc-500 focus-visible:ring dark:ring-zinc-200 dark:focus:outline-none"
|
||||
href="/"
|
||||
aria-label="Brand"
|
||||
>
|
||||
<BrandLogo class="h-full w-auto object-cover rounded-full" />
|
||||
</a>
|
||||
|
||||
<div class="ml-auto md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-none"
|
||||
data-hs-collapse="#navbar-collapse-with-animation"
|
||||
aria-controls="navbar-collapse-with-animation"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<svg
|
||||
class="h-[1.25rem] w-[1.25rem] flex-shrink-0 hs-collapse-open:hidden"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="3" x2="21" y1="6" y2="6"></line>
|
||||
<line x1="3" x2="21" y1="12" y2="12"></line>
|
||||
<line x1="3" x2="21" y1="18" y2="18"></line>
|
||||
</svg>
|
||||
<svg
|
||||
class="hidden h-[1.25rem] w-[1.25rem] flex-shrink-0 hs-collapse-open:block"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="inline-block md:hidden">
|
||||
<Theme />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="navbar-collapse-with-animation"
|
||||
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block"
|
||||
>
|
||||
<div
|
||||
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 lg:gap-x-7 md:gap-y-0 md:ps-7"
|
||||
>
|
||||
{NavigationLinks.map(link => (
|
||||
<NavLink url={link.url} name={link.name} />
|
||||
))}
|
||||
<span class="hidden md:inline-block">
|
||||
<Theme />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<script is:inline>
|
||||
const HSThemeAppearance = {
|
||||
init() {
|
||||
const defaultTheme = "default";
|
||||
let theme = localStorage.getItem("hs_theme") || defaultTheme;
|
||||
|
||||
if (document.querySelector("html").classList.contains("dark")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setAppearance(theme);
|
||||
},
|
||||
|
||||
_resetStylesOnLoad() {
|
||||
const $resetStyles = document.createElement("style");
|
||||
$resetStyles.innerText = `*{transition: unset !important;}`;
|
||||
$resetStyles.setAttribute("data-hs-appearance-onload-styles", "");
|
||||
document.head.appendChild($resetStyles);
|
||||
return $resetStyles;
|
||||
},
|
||||
|
||||
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
|
||||
const $resetStylesEl = this._resetStylesOnLoad();
|
||||
|
||||
if (saveInStore) {
|
||||
localStorage.setItem("hs_theme", theme);
|
||||
}
|
||||
|
||||
if (theme === "auto") {
|
||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "default";
|
||||
}
|
||||
|
||||
document.querySelector("html").classList.remove("dark");
|
||||
document.querySelector("html").classList.remove("default");
|
||||
document.querySelector("html").classList.remove("auto");
|
||||
|
||||
document.querySelector("html").classList.add(this.getOriginalAppearance());
|
||||
|
||||
setTimeout(() => $resetStylesEl.remove());
|
||||
|
||||
if (dispatchEvent) {
|
||||
window.dispatchEvent(new CustomEvent("on-hs-appearance-change", { detail: theme }));
|
||||
}
|
||||
},
|
||||
|
||||
getAppearance() {
|
||||
let theme = this.getOriginalAppearance();
|
||||
if (theme === "auto") {
|
||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "default";
|
||||
}
|
||||
|
||||
return theme;
|
||||
},
|
||||
|
||||
getOriginalAppearance() {
|
||||
const defaultTheme = "default";
|
||||
return localStorage.getItem("hs_theme") || defaultTheme;
|
||||
},
|
||||
};
|
||||
|
||||
HSThemeAppearance.init();
|
||||
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
||||
if (HSThemeAppearance.getOriginalAppearance() === "auto") {
|
||||
HSThemeAppearance.setAppearance("auto", false);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const $clickableThemes = document.querySelectorAll(
|
||||
"[data-hs-theme-click-value]",
|
||||
);
|
||||
const $switchableThemes = document.querySelectorAll(
|
||||
"[data-hs-theme-switch]",
|
||||
);
|
||||
|
||||
$clickableThemes.forEach(($item) => {
|
||||
$item.addEventListener("click", () =>
|
||||
HSThemeAppearance.setAppearance(
|
||||
$item.getAttribute("data-hs-theme-click-value"),
|
||||
true,
|
||||
$item,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
$switchableThemes.forEach(($item) => {
|
||||
$item.addEventListener("change", (e) => {
|
||||
HSThemeAppearance.setAppearance(e.target.checked ? "dark" : "default");
|
||||
});
|
||||
|
||||
$item.checked = HSThemeAppearance.getAppearance() === "dark";
|
||||
});
|
||||
|
||||
window.addEventListener("on-hs-appearance-change", (e) => {
|
||||
$switchableThemes.forEach(($item) => {
|
||||
$item.checked = e.detail === "dark";
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script is:inline src="/vendor/preline/collapse2.1.0.min.js"></script>
|
||||
24
src/components/HeaderLink.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import type { HTMLAttributes } from 'astro/types'
|
||||
|
||||
type Props = HTMLAttributes<'a'>
|
||||
|
||||
const { href, class: className, ...props } = Astro.props
|
||||
|
||||
const { pathname } = Astro.url
|
||||
const isActive = href === pathname || href === pathname.replace(/\/$/, '');
|
||||
---
|
||||
|
||||
<a href={href} class:list={[className, { active: isActive }]} {...props}>
|
||||
<slot />
|
||||
</a>
|
||||
<style>
|
||||
a {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.active {
|
||||
font-weight: bolder;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
54
src/components/blocks/FeaturesSection.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
---
|
||||
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
|
||||
<div class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24">
|
||||
<div class="max-w-5xl px-4 sm:px-6 lg:px-8 mx-auto">
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-6">
|
||||
<a class="group flex flex-col bg-neutral-100 dark:bg-neutral-900/30 rounded-xl hover:group" href="#">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex">
|
||||
<svg class="mt-1 flex-shrink-0 size-5 text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5.8 11.3 2 22l10.7-3.79" /><path d="M4 3h.01" /><path d="M22 8h.01" /><path d="M15 2h.01" /><path d="M22 20h.01" /><path d="m22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12v0c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10" /><path d="m22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11v0c-.11.7-.72 1.22-1.43 1.22H17" /><path d="m11 2 .33.82c.34.86-.2 1.82-1.11 1.98v0C9.52 4.9 9 5.52 9 6.23V7" /><path d="M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z" /></svg>
|
||||
<div class="grow ms-5">
|
||||
<span class="block text-lg font-bold text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300">Telegram канал</span>
|
||||
<span class="mt-1 block text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-400">
|
||||
Там вы выкладываем все самое свежее
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="group flex flex-col bg-neutral-100 dark:bg-neutral-900/30 rounded-xl hover:group" href="#">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex">
|
||||
<svg class="mt-1 flex-shrink-0 size-5 text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -.11376601 49.74245785 51.31690859" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m49.626 11.564a.809.809 0 0 1 .028.209v10.972a.8.8 0 0 1 -.402.694l-9.209 5.302v10.509c0 .286-.152.55-.4.694l-19.223 11.066c-.044.025-.092.041-.14.058-.018.006-.035.017-.054.022a.805.805 0 0 1 -.41 0c-.022-.006-.042-.018-.063-.026-.044-.016-.09-.03-.132-.054l-19.219-11.066a.801.801 0 0 1 -.402-.694v-32.916c0-.072.01-.142.028-.21.006-.023.02-.044.028-.067.015-.042.029-.085.051-.124.015-.026.037-.047.055-.071.023-.032.044-.065.071-.093.023-.023.053-.04.079-.06.029-.024.055-.05.088-.069h.001l9.61-5.533a.802.802 0 0 1 .8 0l9.61 5.533h.002c.032.02.059.045.088.068.026.02.055.038.078.06.028.029.048.062.072.094.017.024.04.045.054.071.023.04.036.082.052.124.008.023.022.044.028.068a.809.809 0 0 1 .028.209v20.559l8.008-4.611v-10.51c0-.07.01-.141.028-.208.007-.024.02-.045.028-.068.016-.042.03-.085.052-.124.015-.026.037-.047.054-.071.024-.032.044-.065.072-.093.023-.023.052-.04.078-.06.03-.024.056-.05.088-.069h.001l9.611-5.533a.801.801 0 0 1 .8 0l9.61 5.533c.034.02.06.045.09.068.025.02.054.038.077.06.028.029.048.062.072.094.018.024.04.045.054.071.023.039.036.082.052.124.009.023.022.044.028.068zm-1.574 10.718v-9.124l-3.363 1.936-4.646 2.675v9.124l8.01-4.611zm-9.61 16.505v-9.13l-4.57 2.61-13.05 7.448v9.216zm-36.84-31.068v31.068l17.618 10.143v-9.214l-9.204-5.209-.003-.002-.004-.002c-.031-.018-.057-.044-.086-.066-.025-.02-.054-.036-.076-.058l-.002-.003c-.026-.025-.044-.056-.066-.084-.02-.027-.044-.05-.06-.078l-.001-.003c-.018-.03-.029-.066-.042-.1-.013-.03-.03-.058-.038-.09v-.001c-.01-.038-.012-.078-.016-.117-.004-.03-.012-.06-.012-.09v-21.483l-4.645-2.676-3.363-1.934zm8.81-5.994-8.007 4.609 8.005 4.609 8.006-4.61-8.006-4.608zm4.164 28.764 4.645-2.674v-20.096l-3.363 1.936-4.646 2.675v20.096zm24.667-23.325-8.006 4.609 8.006 4.609 8.005-4.61zm-.801 10.605-4.646-2.675-3.363-1.936v9.124l4.645 2.674 3.364 1.937zm-18.422 20.561 11.743-6.704 5.87-3.35-8-4.606-9.211 5.303-8.395 4.833z" /></svg>
|
||||
<div class="grow ms-5">
|
||||
<span class="block text-lg font-bold text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300">Telegram группа</span>
|
||||
<span class="mt-1 block text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-400">
|
||||
Тут можно пообщаться без пафоса
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="group flex flex-col bg-neutral-100 dark:bg-neutral-900/30 rounded-xl hover:group" href="#">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex">
|
||||
<svg class="mt-1 flex-shrink-0 size-5 text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z" /><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10" /></svg>
|
||||
<div class="grow ms-5">
|
||||
<span class="block text-lg font-bold text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300">
|
||||
Email
|
||||
</span>
|
||||
<span class="mt-1 block text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-400">
|
||||
И по старики написать на почту <span class="text-blue-600 decoration-2 group-hover:underline font-medium dark:text-blue-500">inbox@stusla.ru</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
60
src/components/blocks/HeroSection.astro
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import PrimaryCTA from '@components/buttons/PrimaryCTA.astro'
|
||||
import SecondaryCTA from '@components/buttons/SecondaryCTA.astro'
|
||||
|
||||
const {
|
||||
title,
|
||||
subTitle,
|
||||
primaryBtn,
|
||||
primaryBtnURL,
|
||||
secondaryBtn,
|
||||
secondaryBtnURL,
|
||||
src,
|
||||
alt,
|
||||
} = Astro.props
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
subTitle?: string
|
||||
primaryBtn?: string
|
||||
primaryBtnURL?: string
|
||||
secondaryBtn?: string
|
||||
secondaryBtnURL?: string
|
||||
src?: any
|
||||
alt?: string
|
||||
}
|
||||
---
|
||||
|
||||
<section class="mx-auto grid max-w-[85rem] gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full">
|
||||
<div>
|
||||
<h1 class="block text-balance text-3xl font-bold tracking-tight text-neutral-800 dark:text-neutral-200 sm:text-4xl lg:text-6xl lg:leading-tight">
|
||||
<Fragment set:html={title} />
|
||||
</h1>
|
||||
{subTitle && (
|
||||
<p class="mt-6 text-pretty text-lg leading-relaxed text-neutral-700 dark:text-neutral-400 lg:w-4/5">
|
||||
{subTitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div class="mt-7 grid w-full gap-3 sm:inline-flex">
|
||||
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
|
||||
{secondaryBtn && <SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full hidden md:block">
|
||||
<div class="top-12 overflow-hidden w-full md:ml-4 flex justify-center">
|
||||
{src && alt && (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
class="h-full w-[320px] scale-100 object-cover object-center"
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
format="avif"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
140
src/components/blocks/HeroSectionAlt.astro
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
import GithubBtn from '@components/buttons/GithubBtn.astro'
|
||||
|
||||
const { title, subTitle, url } = Astro.props
|
||||
|
||||
const btnTitle = 'Continue with Github'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
subTitle?: string
|
||||
url?: string
|
||||
}
|
||||
---
|
||||
|
||||
<section class="relative mx-auto max-w-[85rem] px-4 pb-24 pt-10 sm:px-6 lg:px-8">
|
||||
<div class="absolute left-0 top-[55%] scale-90 md:top-[20%] xl:left-[10%] xl:top-[25%]">
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
color="#ea580c"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#ea580c"
|
||||
stroke="#ea580c"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 18a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
></path>
|
||||
<path
|
||||
stroke="#ea580c"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 7.353v9.294a.6.6 0 0 1-.309.525l-8.4 4.666a.6.6 0 0 1-.582 0l-8.4-4.666A.6.6 0 0 1 3 16.647V7.353a.6.6 0 0 1 .309-.524l8.4-4.667a.6.6 0 0 1 .582 0l8.4 4.667a.6.6 0 0 1 .309.524Z"
|
||||
></path>
|
||||
<path
|
||||
stroke="#ea580c"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m3.528 7.294 8.18 4.544a.6.6 0 0 0 .583 0l8.209-4.56M12 21v-9"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute left-[85%] top-0 scale-75">
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
color="#fbbf24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="#fbbf24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#fbbf24"
|
||||
stroke="#fbbf24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
></path>
|
||||
<path
|
||||
stroke="#fbbf24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 10.5V9M5 15v-1.5"
|
||||
></path>
|
||||
<path
|
||||
fill="#fbbf24"
|
||||
stroke="#fbbf24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM19 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
></path>
|
||||
<path
|
||||
stroke="#fbbf24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M10.5 19H9M15 19h-1.5"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
|
||||
>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
color="#a3a3a3"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="#a3a3a3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.164 17c.29-1.049.67-2.052 1.132-3M11.5 7.794A16.838 16.838 0 0 1 14 6.296M4.5 22a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
|
||||
></path>
|
||||
<path
|
||||
stroke="#a3a3a3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.5 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM19.5 7a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Hero Section Heading -->
|
||||
<div class="mx-auto mt-5 max-w-xl text-center">
|
||||
<h2
|
||||
class="block text-balance text-4xl font-bold leading-tight tracking-tight text-neutral-800 dark:text-neutral-200 md:text-5xl lg:text-6xl"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<!-- Hero Section Sub-heading -->
|
||||
<div class="mx-auto mt-5 max-w-3xl text-center">
|
||||
{
|
||||
subTitle && (
|
||||
<p class="text-pretty text-lg text-neutral-600 dark:text-neutral-400">
|
||||
{subTitle}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<!-- Github Button -->
|
||||
{
|
||||
url && (
|
||||
<div class="mt-8 flex justify-center gap-3">
|
||||
<GithubBtn url={url} title={btnTitle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
34
src/components/blocks/MainSection.astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/buttons/PrimaryCTA.astro'
|
||||
|
||||
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
subTitle: string
|
||||
btnExists?: boolean
|
||||
btnTitle?: string
|
||||
btnURL?: string
|
||||
}
|
||||
---
|
||||
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
|
||||
<div class="md:flex md:justify-between md:items-center flex-wrap">
|
||||
<div class="w-full md:w-auto">
|
||||
<h1 class="block text-balance text-4xl font-bold tracking-tight text-neutral-800 dark:text-neutral-200 md:text-5xl lg:text-6xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p class="mt-4 text-pretty text-lg text-neutral-600 dark:text-neutral-400">
|
||||
{subTitle}
|
||||
</p>
|
||||
</div>
|
||||
{
|
||||
btnExists
|
||||
? (
|
||||
<div class="md:w-auto mt-4 md:mt-0">
|
||||
<PrimaryCTA title={btnTitle} url={btnURL} />
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
34
src/components/blog/BlogCard.astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { Image } from 'astro:assets'
|
||||
import { formatDate } from '../../support/time'
|
||||
|
||||
const { blog } = Astro.props
|
||||
|
||||
interface Props {
|
||||
blog: CollectionEntry<'posts'>
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
class="group relative block rounded-xl outline-none ring-zinc-500 transition duration-500 focus-visible:ring dark:ring-zinc-200 dark:focus:outline-none"
|
||||
href={`/posts/${blog.slug}/`}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="relative w-full flex-shrink-0 overflow-hidden rounded-xl before:absolute before:inset-x-0 before:z-[1] before:size-full before:bg-gradient-to-t before:from-neutral-900/[.7]">
|
||||
<Image
|
||||
class="w-full h-full sm:h-[220px] md:h-[240px] start-0 top-0 size-full object-cover transition duration-500 group-hover:scale-110"
|
||||
src={blog.data.cardImage}
|
||||
alt={blog.data.title}
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
format="avif"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="mt-2 text-xl font-bold text-neutral-800 group-hover:text-orange-400 dark:text-neutral-200 dark:group-hover:text-orange-300">
|
||||
{blog.data.title}
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400 dark:group-hover:text-neutral-500">
|
||||
{formatDate(blog.data.pubDate)}
|
||||
</p>
|
||||
</a>
|
||||
66
src/components/blog/BlogCategory.astro
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
|
||||
const { slug, title, description, count, publishDate } = Astro.props
|
||||
|
||||
interface Props {
|
||||
slug: string
|
||||
title: string
|
||||
description: string
|
||||
count: number
|
||||
publishDate: string
|
||||
}
|
||||
---
|
||||
|
||||
<a href=`/categories/${slug}` class="flex flex-col justify-between size-full bg-neutral-100 shadow-lg rounded-lg p-5 dark:bg-neutral-900/30 group">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div class="flex justify-center gap-x-4 items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<h3 class="text-lg font-semibold tracking-tight text-neutral-600 group-hover:text-orange-400 dark:text-neutral-300 dark:group-hover:text-orange-300">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="block text-neutral-500 min-h-[60px] group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div class="flex mt-2 mb-0 gap-x-5">
|
||||
<span class="text-xs inline-flex items-center rounded me-2 text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-400">
|
||||
<svg
|
||||
class="w-3 h-3 me-1.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="8" height="4" x="8" y="2" rx="1" />
|
||||
<path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-.5" />
|
||||
<path d="M16 4h2a2 2 0 0 1 1.73 1" />
|
||||
<path d="M8 18h1" />
|
||||
<path d="M18.4 9.6a2 2 0 0 1 3 3L17 17l-4 1 1-4Z" />
|
||||
</svg>
|
||||
{count} posts
|
||||
</span>
|
||||
<span class="text-xs inline-flex items-center rounded me-2 text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-400">
|
||||
<svg
|
||||
class="w-3 h-3 me-1.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
update {publishDate}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
44
src/components/blog/BlogInsight.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import Icon from '@components/icons/icon.astro'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
const { blog } = Astro.props
|
||||
|
||||
interface Props {
|
||||
blog: CollectionEntry<'posts'>
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
class="group outline-none rounded-xl ring-zinc-500 transition duration-300 focus-visible:ring dark:ring-zinc-200 dark:focus:outline-none"
|
||||
href={`/posts/${blog.slug}/`}
|
||||
>
|
||||
<div class="aspect-w-16 aspect-h-10 relative overflow-hidden rounded-xl ">
|
||||
<Image
|
||||
class="h-full sm:h-[220px] md:h-[240px] w-full object-cover rounded-xl transition duration-500 ease-in-out group-hover:scale-105"
|
||||
src={blog.data.cardImage}
|
||||
alt={blog.data.title}
|
||||
format="avif"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-7">
|
||||
<h3
|
||||
class="text-xl font-bold text-neutral-800 group-hover:text-neutral-600 dark:text-neutral-200 dark:group-hover:text-neutral-400"
|
||||
>
|
||||
{blog.data.title}
|
||||
</h3>
|
||||
|
||||
<p class="mt-3 text-neutral-600 dark:text-neutral-400">
|
||||
{blog.data.description}
|
||||
</p>
|
||||
|
||||
<p
|
||||
class="mt-5 inline-flex items-center gap-x-1 font-medium text-orange-400 decoration-2 group-hover:underline dark:text-orange-300"
|
||||
>
|
||||
Read more <Icon name="arrowRightStatic" />
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
44
src/components/blog/BlogRecentCard.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import LeftSection from './blocks/LeftSection.astro'
|
||||
import RightSection from './blocks/RightSection.astro'
|
||||
|
||||
const { blogs } = Astro.props
|
||||
|
||||
interface Props {
|
||||
blogs: CollectionEntry<'posts'>[]
|
||||
}
|
||||
|
||||
const posts = blogs.slice(0, 5)
|
||||
---
|
||||
|
||||
{
|
||||
posts.map((b, index) => index % 2 === 0
|
||||
? (
|
||||
<LeftSection
|
||||
title={b.data.title}
|
||||
subTitle={b.data.description}
|
||||
btnExists={true}
|
||||
btnTitle="Read More"
|
||||
btnURL=`/posts/${b.slug}`
|
||||
img={b.data.cardImage}
|
||||
imgAlt={b.data.title}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<RightSection
|
||||
title={b.data.title}
|
||||
subTitle={b.data.description}
|
||||
btnExists={true}
|
||||
btnTitle="Read More"
|
||||
btnURL=`/posts/${b.slug}`
|
||||
img={b.data.cardImage}
|
||||
imgAlt={b.data.title}
|
||||
single={!b.data.cardImage2}
|
||||
imgOne={b.data.cardImage}
|
||||
imgOneAlt={b.data.title}
|
||||
imgTwo={b.data?.cardImage2}
|
||||
imgTwoAlt={b.data.title}
|
||||
/>
|
||||
))
|
||||
}
|
||||
38
src/components/blog/BlogSelectedArticle.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import Icon from '../icons/icon.astro'
|
||||
import BlogCard from './BlogCard.astro'
|
||||
|
||||
const { posts } = Astro.props
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry<'posts'>[]
|
||||
}
|
||||
---
|
||||
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 mb-10 2xl:max-w-full">
|
||||
<div class="text-left">
|
||||
<h2 class="mb-4 text-balance text-5xl font-extrabold tracking-tight text-neutral-800 dark:text-neutral-200">
|
||||
Наши посты
|
||||
</h2>
|
||||
<p class="mb-8 max-w-prose text-pretty font-light text-neutral-600 dark:text-neutral-400 sm:text-xl">
|
||||
Прочитав наши посты мы надеямся что вы разделити нашу радость и удовольствие от работы
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 grid-cols-1 lg:grid-cols-3 sm:grid-cols-2">
|
||||
{posts.map(b => <BlogCard blog={b} />)}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<a
|
||||
href="/timeline"
|
||||
class="group inline-flex items-center justify-center gap-x-2 rounded-full px-4 py-3 text-sm font-bold text-neutral-50 ring-zinc-500 transition duration-300 focus-visible:ring outline-none border border-transparent bg-orange-400 hover:bg-orange-500 active:bg-orange-500 dark:focus:outline-none disabled:pointer-events-none disabled:opacity-50 2xl:text-base dark:ring-zinc-200"
|
||||
>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p>Если что,</p>тут есть ещё
|
||||
<Icon name="arrowRight" class="h-4 w-4 flex-shrink-0 transition duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
30
src/components/blog/BlogTimeline.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
const { blog } = Astro.props
|
||||
|
||||
interface Props {
|
||||
blog: CollectionEntry<'posts'>
|
||||
}
|
||||
---
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="flex gap-x-2">
|
||||
<div class="relative -ml-2 last:after:hidden after:absolute after:top-7 after:bottom-0 after:start-3.5 after:w-px after:-translate-x-[0.5px] after:bg-gray-200 dark:after:bg-neutral-700 after:bg-neutral-400">
|
||||
<div class="relative z-10 size-7 flex justify-center items-center">
|
||||
<div class="size-2 rounded-full bg-neutral-400 dark:bg-neutral-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow pt-0.5 pb-8 group">
|
||||
<a
|
||||
href=`/posts/${blog.slug}`
|
||||
data-astro-prefetch
|
||||
class="flex justify-between text-base font-bold text-neutral-700 outline-none ring-zinc-500 focus:outline-none focus-visible:ring focus-visible:ring-zinc-500 dark:ring-zinc-200"
|
||||
>
|
||||
<h3 class="flex gap-x-1.5 text-xl font-bold text-neutral-800 group-hover:text-orange-600 dark:text-neutral-200 dark:group-hover:text-orange-300">{blog.data.title}</h3>
|
||||
<!--<span class="tracking-wide text-xs font-medium text-neutral-600 dark:text-neutral-500 group-hover:text-neutral-600 dark:group-hover:text-neutral-400">{formatDate(blog.data.pubDate)}</span>-->
|
||||
</a>
|
||||
<p class="mt-1.5 text-base text-neutral-600 group-hover:text-neutral-500 dark:text-neutral-500 dark:group-hover:text-neutral-400">{blog.data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
40
src/components/blog/blocks/LeftSection.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/buttons/PrimaryCTA.astro'
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.props
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
subTitle: string
|
||||
btnExists?: boolean
|
||||
btnTitle?: string
|
||||
btnURL?: string
|
||||
img: any
|
||||
imgAlt: any
|
||||
}
|
||||
---
|
||||
|
||||
<section
|
||||
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
|
||||
>
|
||||
<Image
|
||||
class="w-full rounded-xl"
|
||||
src={img}
|
||||
alt={imgAlt}
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<div class="mt-4 md:mt-0">
|
||||
<h2 class="mb-4 text-balance text-4xl font-extrabold tracking-tight text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
<p class="mb-4 max-w-prose text-pretty font-light text-neutral-600 dark:text-neutral-400 sm:text-lg">
|
||||
{subTitle}
|
||||
</p>
|
||||
{
|
||||
btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
79
src/components/blog/blocks/RightSection.astro
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/buttons/PrimaryCTA.astro'
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
const {
|
||||
title,
|
||||
subTitle,
|
||||
btnExists,
|
||||
btnTitle,
|
||||
btnURL,
|
||||
single,
|
||||
imgOne,
|
||||
imgOneAlt,
|
||||
imgTwo,
|
||||
imgTwoAlt,
|
||||
} = Astro.props
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
subTitle: string
|
||||
btnExists?: boolean
|
||||
btnTitle?: string
|
||||
btnURL?: string
|
||||
single?: boolean
|
||||
imgOne?: any
|
||||
imgOneAlt?: any
|
||||
imgTwo?: any
|
||||
imgTwoAlt?: any
|
||||
}
|
||||
---
|
||||
|
||||
<section class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full">
|
||||
<div>
|
||||
<h2 class="mb-4 text-balance text-4xl font-extrabold tracking-tight text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
<p class="mb-4 max-w-prose text-pretty font-light text-neutral-600 dark:text-neutral-400 sm:text-lg">
|
||||
{subTitle}
|
||||
</p>
|
||||
{
|
||||
btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
single
|
||||
? (
|
||||
<div class="mt-8">
|
||||
<Image
|
||||
class="w-full rounded-lg"
|
||||
src={imgOne}
|
||||
alt={imgOneAlt}
|
||||
format="avif"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div class="mt-8 grid grid-cols-2 gap-4">
|
||||
<Image
|
||||
class="w-full rounded-xl"
|
||||
src={imgOne}
|
||||
alt={imgOneAlt}
|
||||
draggable="false"
|
||||
format="avif"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Image
|
||||
class="mt-4 w-full rounded-xl lg:mt-10"
|
||||
src={imgTwo}
|
||||
alt={imgTwoAlt}
|
||||
draggable="false"
|
||||
format="avif"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
98
src/components/buttons/Bookmark.astro
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
import Icon from '@components/icons/icon.astro';
|
||||
---
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 outline-none ring-zinc-500 transition duration-300 hover:bg-neutral-100 focus:outline-none focus-visible:outline-none focus-visible:ring-1 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700"
|
||||
data-bookmark-button="bookmark-button"
|
||||
>
|
||||
<Icon name="bookmark" />
|
||||
</button>
|
||||
|
||||
<script>
|
||||
class Bookmark {
|
||||
private static readonly BOOKMARKS_KEY = "bookmarks";
|
||||
private bookmarkButton: Element | null;
|
||||
|
||||
constructor(private dataAttrValue: string) {
|
||||
this.bookmarkButton = document.querySelector(
|
||||
`[data-bookmark-button="${dataAttrValue}"]`
|
||||
);
|
||||
}
|
||||
|
||||
private getStoredBookmarks(): string[] {
|
||||
const item = localStorage.getItem(Bookmark.BOOKMARKS_KEY);
|
||||
return item ? JSON.parse(item) : [];
|
||||
}
|
||||
|
||||
init(): void {
|
||||
if (this.bookmarkButton && this.isStored()) {
|
||||
this.markAsStored();
|
||||
}
|
||||
|
||||
this.bookmarkButton?.addEventListener("click", () =>
|
||||
this.toggleBookmark()
|
||||
);
|
||||
}
|
||||
|
||||
isStored(): boolean {
|
||||
return this.getStoredBookmarks().includes(window.location.pathname);
|
||||
}
|
||||
|
||||
markAsStored(): void {
|
||||
if (this.bookmarkButton) {
|
||||
this.bookmarkButton.classList.add("bookmarked");
|
||||
let svgElement = this.bookmarkButton.querySelector("svg");
|
||||
if (svgElement) {
|
||||
svgElement.setAttribute(
|
||||
"class",
|
||||
"h-6 w-6 fill-red-500 dark:fill-red-500"
|
||||
);
|
||||
}
|
||||
let pathElement = svgElement?.querySelector("path");
|
||||
if (pathElement) {
|
||||
pathElement.setAttribute(
|
||||
"class",
|
||||
"fill-current text-red-500 dark:text-red-500"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unmarkAsStored(): void {
|
||||
if (this.bookmarkButton) {
|
||||
this.bookmarkButton.classList.remove("bookmarked");
|
||||
let svgElement = this.bookmarkButton.querySelector("svg");
|
||||
if (svgElement) {
|
||||
svgElement.setAttribute("class", "h-6 w-6 fill-none");
|
||||
}
|
||||
let pathElement = svgElement?.querySelector("path");
|
||||
if (pathElement) {
|
||||
pathElement.setAttribute(
|
||||
"class",
|
||||
"fill-current text-neutral-500 group-hover:text-red-400 dark:text-neutral-500 group-hover:dark:text-red-400"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleBookmark(): void {
|
||||
let storedBookmarks = this.getStoredBookmarks();
|
||||
const index = storedBookmarks.indexOf(window.location.pathname);
|
||||
if (index !== -1) {
|
||||
storedBookmarks.splice(index, 1);
|
||||
this.unmarkAsStored();
|
||||
} else {
|
||||
storedBookmarks.push(window.location.pathname);
|
||||
this.markAsStored();
|
||||
}
|
||||
localStorage.setItem(
|
||||
Bookmark.BOOKMARKS_KEY,
|
||||
JSON.stringify(storedBookmarks)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new Bookmark("bookmark-button").init();
|
||||
</script>
|
||||
27
src/components/buttons/GithubBtn.astro
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import Icon from '@components/icons/icon.astro'
|
||||
|
||||
const { title, url } = Astro.props
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const baseClasses = 'group inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-700 ring-zinc-500 focus-visible:ring transition duration-300 outline-none'
|
||||
const borderClasses = 'border border-transparent'
|
||||
const bgColorClasses = 'bg-yellow-400 dark:focus:outline-none'
|
||||
const hoverClasses = 'hover:shadow-2xl hover:shadow-yellow-500'
|
||||
const fontSizeClasses = '2xl:text-base'
|
||||
const ringClasses = 'dark:ring-zinc-200';
|
||||
---
|
||||
|
||||
<a
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${fontSizeClasses} ${ringClasses}`}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="github" />
|
||||
{title}
|
||||
</a>
|
||||
23
src/components/buttons/NotFoundButton.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import Icon from '@components/icons/icon.astro'
|
||||
|
||||
const { title, id, noArrow } = Astro.props
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
id?: string
|
||||
noArrow?: boolean
|
||||
}
|
||||
|
||||
const baseClasses = 'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-zinc-500 transition duration-300 focus-visible:ring outline-none'
|
||||
const borderClasses = 'border border-transparent'
|
||||
const bgColorClasses = 'bg-orange-400 hover:bg-orange-500 active:bg-orange-500 dark:focus:outline-none'
|
||||
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50'
|
||||
const fontSizeClasses = '2xl:text-base'
|
||||
const ringClasses = 'dark:ring-zinc-200';
|
||||
---
|
||||
|
||||
<button class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`} id={id}>
|
||||
{title}
|
||||
{noArrow ? null : <Icon name="arrowRight" />}
|
||||
</button>
|
||||
26
src/components/buttons/PrimaryCTA.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import Icon from '@components/icons/icon.astro'
|
||||
|
||||
const { title, url, noArrow } = Astro.props
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
url?: string
|
||||
noArrow?: boolean
|
||||
}
|
||||
|
||||
const baseClasses = 'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-zinc-500 transition duration-300 focus-visible:ring outline-none'
|
||||
const borderClasses = 'border border-transparent'
|
||||
const bgColorClasses = 'bg-orange-400 hover:bg-orange-500 active:bg-orange-500 dark:focus:outline-none'
|
||||
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50'
|
||||
const fontSizeClasses = '2xl:text-base'
|
||||
const ringClasses = 'dark:ring-zinc-200';
|
||||
---
|
||||
|
||||
<a
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
{noArrow ? null : <Icon name="arrowRight" />}
|
||||
</a>
|
||||
24
src/components/buttons/SecondaryCTA.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
const { title, url } = Astro.props
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const baseClasses = 'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-zinc-500 focus-visible:ring transition duration-300'
|
||||
const borderClasses = 'border border-neutral-200'
|
||||
const bgColorClasses = 'bg-neutral-300'
|
||||
const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700'
|
||||
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50'
|
||||
const fontSizeClasses = '2xl:text-base'
|
||||
const ringClasses = 'ring-zinc-500'
|
||||
const darkClasses = 'dark:border-neutral-700 dark:bg-zinc-700 dark:text-neutral-300 dark:ring-zinc-200 dark:hover:bg-zinc-600 dark:focus:outline-none';
|
||||
---
|
||||
|
||||
<a
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
151
src/components/buttons/SocialShare.astro
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
import Icon from '@components/icons/icon.astro'
|
||||
|
||||
const { pageTitle, title = 'Share' } = Astro.props
|
||||
|
||||
interface Props {
|
||||
pageTitle: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
type SocialPlatform = {
|
||||
name: string
|
||||
url: string
|
||||
svg: string
|
||||
}
|
||||
|
||||
const socialPlatforms: SocialPlatform[] = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`,
|
||||
svg: 'facebook',
|
||||
},
|
||||
{
|
||||
name: 'X',
|
||||
url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`,
|
||||
svg: 'x',
|
||||
},
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`,
|
||||
svg: 'linkedIn',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]">
|
||||
<button
|
||||
id="hs-dropup"
|
||||
type="button"
|
||||
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 outline-none ring-zinc-500 transition duration-300 hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none"
|
||||
>
|
||||
<Icon name="share" />
|
||||
|
||||
{title}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="hs-dropdown-menu duration z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] hs-dropdown-open:opacity-100 dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
|
||||
aria-labelledby="hs-dropup"
|
||||
>
|
||||
<div class="py-2 first:pt-0 last:pb-0">
|
||||
{
|
||||
socialPlatforms.map(platform => (
|
||||
<a
|
||||
class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700 "
|
||||
href={platform.url}
|
||||
>
|
||||
<Icon name={platform.svg} />
|
||||
Share on {platform.name}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="py-2 first:pt-0 last:pb-0">
|
||||
<button
|
||||
type="button"
|
||||
class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:outline-none focus-visible:ring-1 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
|
||||
data-clipboard-success-text="Copied"
|
||||
>
|
||||
<svg
|
||||
class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
|
||||
<path
|
||||
d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="js-clipboard-success text-neutral-700 dark:text-neutral-300 hidden h-4 w-4"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span class="js-clipboard-success-text">Copy link</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Import the necessary Dropdown and Clipboard plugins-->
|
||||
<!--https://preline.co/plugins/html/dropdown.html-->
|
||||
<!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>-->
|
||||
|
||||
<!-- https://clipboardjs.com/ -->
|
||||
<!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>-->
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
window.addEventListener("load", () => {
|
||||
const $clipboards = document.querySelectorAll(".js-clipboard");
|
||||
$clipboards.forEach((el) => {
|
||||
const clipboard = new ClipboardJS(el, {
|
||||
text: () => {
|
||||
return window.location.href;
|
||||
},
|
||||
});
|
||||
clipboard.on("success", () => {
|
||||
const $default = el.querySelector(".js-clipboard-default");
|
||||
const $success = el.querySelector(".js-clipboard-success");
|
||||
const $successText = el.querySelector(".js-clipboard-success-text");
|
||||
const successText = el.dataset.clipboardSuccessText || "";
|
||||
let oldSuccessText;
|
||||
|
||||
if ($successText) {
|
||||
oldSuccessText = $successText.textContent;
|
||||
$successText.textContent = successText;
|
||||
}
|
||||
if ($default && $success) {
|
||||
$default.style.display = "none";
|
||||
$success.style.display = "block";
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
if ($successText && oldSuccessText)
|
||||
$successText.textContent = oldSuccessText;
|
||||
if ($default && $success) {
|
||||
$success.style.display = "";
|
||||
$default.style.display = "";
|
||||
}
|
||||
}, 800);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
40
src/components/icons/icon.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { Icons } from './icons.ts'
|
||||
|
||||
interface Path {
|
||||
d: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { name } = Astro.props
|
||||
|
||||
const icon = (Icons as any)[name] || {}
|
||||
|
||||
const paths: Path[] = icon.paths || [];
|
||||
---
|
||||
|
||||
{
|
||||
icon
|
||||
? (
|
||||
<svg
|
||||
class={icon.class}
|
||||
height={icon.height}
|
||||
viewBox={icon.viewBox}
|
||||
width={icon.width}
|
||||
fill={icon.fill}
|
||||
clip-rule={icon.clipRule}
|
||||
fill-rule={icon.fillRule}
|
||||
stroke={icon.stroke}
|
||||
stroke-width={icon.strokeWidth}
|
||||
stroke-linecap={icon.strokeLinecap}
|
||||
stroke-linejoin={icon.strokeLinejoin}
|
||||
><title>{icon.title}</title>
|
||||
{paths.map(path => (
|
||||
<path d={path.d} class={path.class || ''} />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
: (
|
||||
'Icon not found'
|
||||
)
|
||||
}
|
||||
414
src/components/icons/icons.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
export const Icons = {
|
||||
groups: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm150-400 82-80-82-82-80 82 80 80Zm573-10 87-140 88 140H723Zm-243-70q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm.351-180Q455-660 437.5-642.851t-17.5 42.5Q420-575 437.351-557.5t43 17.5Q506-540 523-557.351t17-43Q540-626 522.851-643t-42.5-17ZM480-600ZM0-240v-53q0-39.464 42-63.232T150.398-380q12.158 0 23.38.5T196-377.273q-8 17.273-12 34.842-4 17.57-4 37.431v65H0Zm240 0v-65q0-65 66.5-105T480-450q108 0 174 40t66 105v65H240Zm570-140q67.5 0 108.75 23.768T960-293v53H780v-65q0-19.861-3.5-37.431Q773-360 765-377.273q11-1.727 22.171-2.227 11.172-.5 22.829-.5Zm-330.2-10Q400-390 350-366q-50 24-50 61v5h360v-6q0-36-49.5-60t-130.7-24Zm.2 90Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
books: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M343-420h225v-60H343v60Zm0-90h395v-60H343v60Zm0-90h395v-60H343v60Zm-83 400q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
verified: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm346-60-76-130-151-31 17-147-96-112 96-111-17-147 151-31 76-131 134 62 134-62 77 131 150 31-17 147 96 111-96 112 17 147-150 31-77 130-134-62-134 62Zm27-79 107-45 110 45 67-100 117-30-12-119 81-92-81-94 12-119-117-28-69-100-108 45-110-45-67 100-117 28 12 119-81 94 81 92-12 121 117 28 70 100Zm107-341Zm-43 133 227-225-45-41-182 180-95-99-46 45 141 140Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
frame: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M480-480q-51 0-85.5-34.5T360-600q0-50 34.5-85t85.5-35q50 0 85 35t35 85q0 51-35 85.5T480-480Zm-.351-60Q505-540 522.5-557.149t17.5-42.5Q540-625 522.649-642.5t-43-17.5Q454-660 437-642.649t-17 43Q420-574 437.149-557t42.5 17ZM240-240v-76q0-27 17.5-47.5T300-397q42-22 86.943-32.5 44.942-10.5 93-10.5Q528-440 573-429.5t87 32.5q25 13 42.5 33.5T720-316v76H240Zm240-140q-47.546 0-92.773 13T300-328v28h360v-28q-42-26-87.227-39-45.227-13-92.773-13Zm0-220Zm0 300h180-360 180ZM140-80q-24 0-42-18t-18-42v-172h60v172h172v60H140ZM80-648v-172q0-24 18-42t42-18h172v60H140v172H80ZM648-80v-60h172v-172h60v172q0 24-18 42t-42 18H648Zm172-568v-172H648v-60h172q24 0 42 18t18 42v172h-60Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
tools: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M764-80q-6 0-11-2t-10-7L501-331q-5-5-7-10t-2-11q0-6 2-11t7-10l85-85q5-5 10-7t11-2q6 0 11 2t10 7l242 242q5 5 7 10t2 11q0 6-2 11t-7 10l-85 85q-5 5-10 7t-11 2Zm0-72 43-43-200-200-43 43 200 200ZM195-80q-6 0-11.5-2T173-89l-84-84q-5-5-7-10.5T80-195q0-6 2-11t7-10l225-225h85l38-38-175-175h-57L80-779l99-99 125 125v57l175 175 130-130-67-67 56-56H485l-18-18 128-128 18 18v113l56-56 169 169q15 15 23.5 34.5T870-600q0 20-6.5 38.5T845-528l-85-85-56 56-52-52-211 211v84L216-89q-5 5-10 7t-11 2Zm0-72 200-200v-43h-43L152-195l43 43Zm0 0-43-43 22 21 21 22Zm569 0 43-43-43 43Z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
dashboard: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M510-570v-270h330v270H510ZM120-450v-390h330v390H120Zm390 330v-390h330v390H510Zm-390 0v-270h330v270H120Zm60-390h210v-270H180v270Zm390 330h210v-270H570v270Zm0-450h210v-150H570v150ZM180-180h210v-150H180v150Zm210-330Zm180-120Zm0 180ZM390-330Z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
house: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'h-6 w-6 flex-shrink-0 text-neutral-700 hs-tab-active:text-orange-400 dark:text-neutral-300 dark:hs-tab-active:text-orange-300 md:h-7 md:w-7',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
arrowUp: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm5 12 7-7 7 7',
|
||||
},
|
||||
{
|
||||
d: 'M12 19V5',
|
||||
},
|
||||
],
|
||||
class: 'h-5 w-5 flex-shrink-0 text-orange-400 dark:text-orange-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
checkCircle: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M10 18a8 8 0 100-16 8 8 0 000 16zM13.707 8.293a1 1 0 00-1.414-1.414L9 10.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z',
|
||||
},
|
||||
],
|
||||
class: 'h-5 w-5 shrink-0',
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'currentColor',
|
||||
fillRule: 'evenodd',
|
||||
clipRule: 'evenodd',
|
||||
},
|
||||
bookmark: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z',
|
||||
class:
|
||||
'fill-current text-neutral-500 transition duration-300 group-hover:text-red-400 group-hover:dark:text-red-400',
|
||||
},
|
||||
],
|
||||
class: 'h-6 w-6 fill-none transition duration-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
arrowRight: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm9 18 6-6-6-6',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:translate-x-1',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
facebook: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0 fill-current',
|
||||
viewBox: '0 0 24 24',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
x: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0 fill-current',
|
||||
viewBox: '0 0 24 24',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
linkedIn: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0 fill-current',
|
||||
viewBox: '0 0 24 24',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
share: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 group-hover:text-neutral-700',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
github: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z',
|
||||
},
|
||||
],
|
||||
class: 'w-4.5 h-4.5 transition flex-shrink-0 text-neutral-700 duration-300 group-hover:-translate-y-1',
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 16 16',
|
||||
fill: 'currentColor',
|
||||
},
|
||||
arrowRightStatic: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm9 18 6-6-6-6',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
openInNew: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm4.5 19.5 15-15m0 0H8.25m11.25 0v11.25',
|
||||
},
|
||||
],
|
||||
class: 'ml-0.5 w-3 h-3 md:w-4 md:h-4 inline pb-0.5',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '3',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
accordionNotActive: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm6 9 6 6 6-6',
|
||||
},
|
||||
],
|
||||
class: 'block h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:hidden dark:text-neutral-400',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
accordionActive: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm18 15-6-6-6 6',
|
||||
},
|
||||
],
|
||||
class: 'hidden h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:block dark:text-neutral-400',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
xFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Twitter',
|
||||
},
|
||||
facebookFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Facebook',
|
||||
},
|
||||
githubFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'GitHub',
|
||||
},
|
||||
googleFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Google',
|
||||
},
|
||||
slackFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Slack',
|
||||
},
|
||||
quotation: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z',
|
||||
},
|
||||
],
|
||||
class: 'absolute start-0 top-0 h-16 w-16 -translate-x-6 -translate-y-8 transform text-neutral-300 dark:text-neutral-700',
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 16 16',
|
||||
fill: 'currentColor',
|
||||
},
|
||||
question: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
chatBubble: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
mapPin: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z',
|
||||
},
|
||||
{
|
||||
d: 'M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
envelopeOpen: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
earth: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525',
|
||||
},
|
||||
],
|
||||
class: 'w-4 h-4 flex-shrink-0',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
}
|
||||
14
src/components/logos/BrandLogo.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import logo from '@images/brand-logo.jpeg'
|
||||
import { SITE } from '../../config';
|
||||
---
|
||||
|
||||
<Image
|
||||
src={logo}
|
||||
alt={SITE.title}
|
||||
{...Astro.props}
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
/>
|
||||
|
||||
48
src/components/logos/Theme.astro
Normal file
@@ -0,0 +1,48 @@
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dark Theme Toggle"
|
||||
class="hs-dark-mode group flex h-8 w-8 items-center justify-center rounded-full font-medium text-neutral-600 outline-none ring-zinc-500 transition duration-300 hover:bg-neutral-200 hover:text-orange-400 hs-dark-mode-active:hidden dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-orange-300 dark:focus:outline-none"
|
||||
data-hs-theme-click-value="dark"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Light Theme Toggle"
|
||||
class="hs-dark-mode group hidden h-8 w-8 items-center justify-center rounded-full font-medium text-neutral-600 outline-none ring-zinc-500 transition duration-300 hover:text-orange-400 hs-dark-mode-active:flex dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-orange-300 dark:focus:outline-none"
|
||||
data-hs-theme-click-value="light"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M12 8a2 2 0 1 0 4 4"></path>
|
||||
<path d="M12 2v2"></path>
|
||||
<path d="M12 20v2"></path>
|
||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
||||
<path d="M2 12h2"></path>
|
||||
<path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path>
|
||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
||||
</svg>
|
||||
</button>
|
||||
18
src/components/support/GoogleAnalytics.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import { GoogleAnalytics } from '../../config'
|
||||
|
||||
const ID = GoogleAnalytics.id
|
||||
---
|
||||
|
||||
{
|
||||
GoogleAnalytics.enable && (
|
||||
<script is:inline type="text/partytown" src=`https://www.googletagmanager.com/gtag/js?id=${ID}`></script>
|
||||
<script is:inline define:vars={{ ID }}>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', ID);
|
||||
</script>
|
||||
)
|
||||
}
|
||||
|
||||
15
src/components/ui/FooterSocialLink.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
const { url } = Astro.props
|
||||
interface Props {
|
||||
url: string
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
class="inline-flex h-14 w-14 items-center justify-center gap-x-2 rounded-lg border border-transparent text-sm font-bold text-neutral-700 outline-none ring-zinc-500 hover:bg-neutral-500/10 focus:outline-none focus-visible:ring focus-visible:ring-zinc-500 dark:ring-zinc-200 dark:hover:bg-neutral-50/10"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
44
src/components/ui/NavLink.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
const { url, name } = Astro.props
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
id={url === '/' ? 'home' : url.replace('/', '')}
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
class="rounded-lg text-base font-medium text-neutral-600 outline-none ring-zinc-500 hover:text-neutral-500 focus-visible:ring dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-neutral-500 dark:focus:outline-none md:py-3 md:text-sm 2xl:text-base"
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let url = window.location.pathname;
|
||||
let urlSegments = url.split("/");
|
||||
let navId;
|
||||
|
||||
if (url === "/") {
|
||||
navId = "home";
|
||||
} else {
|
||||
navId = url.replace("/", "");
|
||||
}
|
||||
|
||||
let nav = document.getElementById(navId);
|
||||
|
||||
if (nav) {
|
||||
nav.classList.remove(
|
||||
"text-neutral-600",
|
||||
"dark:text-neutral-400",
|
||||
"hover:text-neutral-500",
|
||||
"dark:hover:text-neutral-500"
|
||||
);
|
||||
nav.classList.add("text-orange-400", "dark:text-orange-300");
|
||||
nav.setAttribute("aria-current", "page");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
72
src/config.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { NavigationLink, Site, User } from './types.ts'
|
||||
|
||||
export const SITE: Site = {
|
||||
author: 'iTKeyS',
|
||||
url: 'https://stusla.ru',
|
||||
title: 'stusla.ru',
|
||||
description: 'Каркасные дома, мебель из дерева, кухни, в стиле лофт',
|
||||
shortDescription: '',
|
||||
}
|
||||
|
||||
export const NavigationLinks: NavigationLink[] = [
|
||||
{ name: 'Посты', url: '/posts' },
|
||||
{ name: 'Категории', url: '/categories' },
|
||||
{ name: 'Хронология', url: '/timeline' },
|
||||
{ name: 'О нас', url: '/posts/about' },
|
||||
{ name: 'Контакты', url: '/friends' },
|
||||
]
|
||||
|
||||
export const Friends: User[] = [
|
||||
{
|
||||
avatar: 'https://tcxx.info/wp-content/themes/StarryW/images/bg/me.jpg',
|
||||
social: { blog: 'https:плата-управления.рф', telegram: 'iTKeyS' },
|
||||
title: 'СисАдмин',
|
||||
name: 'Тихон',
|
||||
description: 'Как ущел за белым кроликом, так хей знает где он ходит',
|
||||
},
|
||||
]
|
||||
|
||||
export const FooterLinks = [
|
||||
{
|
||||
section: 'Блог',
|
||||
links: [
|
||||
{ name: 'Посты', url: '/posts' },
|
||||
{ name: 'Хронология', url: '/timeline' },
|
||||
{ name: 'Категории', url: '/categories' },
|
||||
{ name: 'О нас', url: '/posts/about' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Ссылки',
|
||||
links: [
|
||||
{ name: 'Viber', url: 'https://#' },
|
||||
{ name: "What's App", url: 'https://#' },
|
||||
{ name: 'Telegram', url: 'https://#' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const GoogleAnalytics = {
|
||||
enable: true,
|
||||
id: '11111111111111111',
|
||||
}
|
||||
|
||||
export const SEO = {
|
||||
title: SITE.title,
|
||||
description: SITE.description,
|
||||
structuredData: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'inLanguage': 'ru-RU',
|
||||
'@id': SITE.url,
|
||||
'url': SITE.url,
|
||||
'name': SITE.title,
|
||||
'description': SITE.description,
|
||||
'isPartOf': {
|
||||
'@type': 'WebSite',
|
||||
'url': SITE.url,
|
||||
'name': SITE.title,
|
||||
'description': SITE.description,
|
||||
},
|
||||
},
|
||||
}
|
||||
4
src/content/categories/Home_carcas.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: 'Дома каркасники'
|
||||
description: 'Максимально упрощенное строительство домов из каркаса'
|
||||
---
|
||||
4
src/content/categories/dvor_mebel.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: 'Дворовая мебель'
|
||||
description: 'Простое исполнение дворового декора'
|
||||
---
|
||||
4
src/content/categories/life.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: 'Наша жизнь'
|
||||
description: 'Наши неповторимые истории'
|
||||
---
|
||||
36
src/content/config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineCollection, z } from 'astro:content'
|
||||
// import { Categories } from '../config.ts'
|
||||
// const slugs = Categories.map(c => c.slug)
|
||||
// const categories = z.enum(slugs as [string, ...string[]])
|
||||
|
||||
const posts = defineCollection({
|
||||
schema: ({ image }) => z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
cardImage: image(),
|
||||
cardImage2: image().optional(),
|
||||
ogImage: image()
|
||||
.refine(img => img.width >= 1200 && img.height >= 630, {
|
||||
message: 'OpenGraph image must be at least 1200 X 630 pixels!',
|
||||
})
|
||||
.or(z.string())
|
||||
.optional(),
|
||||
category: z.string(),
|
||||
pubDate: z.coerce.date(),
|
||||
selected: z.boolean().optional(),
|
||||
oldViewCount: z.number().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
const categoryCollection = defineCollection({
|
||||
type: 'content',
|
||||
schema: () => z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const collections = { posts, categories: categoryCollection }
|
||||
21
src/content/posts/01-one-day.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "История первая, пора новой столярки"
|
||||
description: "Хорошенько раскинув мозгами, взвесив все за и против, мы решеи переехать в новый цех..."
|
||||
pubDate: "2024-06-15 13:00:00"
|
||||
category: "life"
|
||||
cardImage: "@images/banners/gblog-2.jpg"
|
||||
cardImage2: "@images/banners/gblog.jpg"
|
||||
tags: ["Наша жизнь"]
|
||||
selected: true
|
||||
---
|
||||
|
||||
Это неболшая история о том что в какой-то момент нашей деятельности нам стало меньше места, а объемы работы начали рости в геометричеакой прогресси. И вот оно тот самый волшебный пинок под зад от вселенной и мы мчим на крыльях турбогазели искить новое гнезно для наших опилок.
|
||||
|
||||
# Вот оно
|
||||
Спустя непродолжительную по чательно исборчиваю операцию по подбору помещений мы наши его, наш новый, грязный но охуенный цех.
|
||||
|
||||
# Действуй Зина
|
||||
отдав на руки кэш владельшу, набрали девчатам из соседнего стиль цеха, заручившись моддржкой мамочки, загнали нездавших вступительные экзамены студенток пидарасить наш новый мебли-ебли цех!
|
||||
|
||||
# Как у кота
|
||||
Девчули так четко нам шлейфанули цех, что мы на рабостях им подогнали словянский шкаф для трусов и юбок...
|
||||
2
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
BIN
src/images/banners/gblog-2.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
src/images/banners/gblog.jpg
Normal file
|
After Width: | Height: | Size: 701 KiB |
BIN
src/images/banners/gpt-2.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/images/banners/gpt.jpg
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
src/images/banners/laravel-2.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
src/images/banners/laravel-3.jpg
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
src/images/banners/markdown.avif
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/images/banners/why-astro-2.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/images/banners/why-astro.jpg
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
src/images/brand-logo.jpeg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/images/footer-ponyo.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
src/images/hero-landing.png
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
src/images/icon.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
15
src/images/icon.svg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/images/ponyo.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
src/images/social-home.png
Normal file
|
After Width: | Height: | Size: 319 KiB |
84
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
import BaseHead from '@components/BaseHead.astro'
|
||||
import Footer from '@components/Footer.astro'
|
||||
import Header from '@components/Header.astro'
|
||||
import GoogleAnalytics from '@components/support/GoogleAnalytics.astro'
|
||||
import { SITE } from '../config'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
description: string
|
||||
ogImage?: any
|
||||
lang?: string
|
||||
structuredData?: object
|
||||
}
|
||||
|
||||
const { title, description = SITE.description, ogImage, lang = 'en', structuredData } = Astro.props
|
||||
const normalizeTitle = !title ? SITE.title : `${title} - ${SITE.title}`
|
||||
---
|
||||
|
||||
<html lang={lang} class="scrollbar-hide lenis lenis-smooth scroll-pt-16">
|
||||
<head>
|
||||
<title>{normalizeTitle}</title>
|
||||
<BaseHead
|
||||
title={normalizeTitle}
|
||||
description={description}
|
||||
ogImage={ogImage}
|
||||
ogTitle={title === '' ? SITE.title : title}
|
||||
ogDescription={description}
|
||||
structuredData={structuredData}
|
||||
/>
|
||||
<script is:inline>
|
||||
if (localStorage.getItem("hs_theme") === "dark"
|
||||
|| (!("hs_theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
</script>
|
||||
<script is:inline src="/vendor/lenis/lenis1.0.42.min.js"></script>
|
||||
<script is:inline>
|
||||
const lenis = new Lenis();
|
||||
function raf(time) {
|
||||
lenis.raf(time)
|
||||
requestAnimationFrame(raf)
|
||||
}
|
||||
requestAnimationFrame(raf)
|
||||
</script>
|
||||
<GoogleAnalytics />
|
||||
</head>
|
||||
<body class="bg-neutral-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-neutral-800">
|
||||
<div class="mx-auto max-w-screen-2xl px-4 sm:px-6 lg:px-8">
|
||||
<Header />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
html.lenis,
|
||||
html.lenis body {
|
||||
height: auto;
|
||||
}
|
||||
.lenis.lenis-smooth {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.lenis.lenis-stopped {
|
||||
overflow: hidden;
|
||||
}
|
||||
.lenis.lenis-scrolling iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
26
src/pages/404.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import PrimaryCTA from '@components/buttons/PrimaryCTA.astro'
|
||||
import { SITE } from '../config'
|
||||
|
||||
const pageTitle: string = `Page Not Found | ${SITE.title}`
|
||||
|
||||
const title: string = 'Страница не найдена'
|
||||
const subTitle: string = 'Ну пиздец'
|
||||
---
|
||||
|
||||
<BaseLayout title={pageTitle}>
|
||||
<section class="grid h-svh place-content-center">
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-8 lg:px-6 lg:py-16">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<h1 class="text-dark mb-4 text-7xl font-extrabold text-yellow-500 dark:text-yellow-400 lg:text-9xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p class="mb-4 text-balance text-3xl font-bold tracking-tight text-neutral-700 dark:text-neutral-300 md:text-4xl">
|
||||
{subTitle}
|
||||
</p>
|
||||
<PrimaryCTA title="Домой" url="/" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
58
src/pages/categories/[...slug].astro
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||
import MainSection from '@components/blocks/MainSection.astro'
|
||||
import BlogCard from '@components/blog/BlogCard.astro'
|
||||
import { SITE } from '../../config'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = await getCollection('categories')
|
||||
return categories.map(category => ({
|
||||
params: { slug: category.slug },
|
||||
props: { category },
|
||||
}))
|
||||
}
|
||||
|
||||
const { category } = Astro.props
|
||||
const URL = Astro.url.href
|
||||
const categoriesURL = `${Astro.url.origin}/categories`
|
||||
|
||||
const posts: CollectionEntry<'posts'>[] = (await getCollection('posts')).sort(
|
||||
(a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
).filter((b) => {
|
||||
return b.data.category === category.slug
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={category.data.title}
|
||||
description={category.data.description}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'inLanguage': 'ru-RU',
|
||||
'@id': URL,
|
||||
'url': URL,
|
||||
'name': `${category.data.title} - ${SITE.title}`,
|
||||
'description': category.data.description,
|
||||
'isPartOf': {
|
||||
'@type': 'WebSite',
|
||||
'url': categoriesURL,
|
||||
'name': `All Categories - ${SITE.title}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MainSection
|
||||
title={category.data.title}
|
||||
subTitle={category.data.description}
|
||||
btnExists={true}
|
||||
btnTitle="Go to List"
|
||||
btnURL="/categories"
|
||||
/>
|
||||
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 mb-10 2xl:max-w-full">
|
||||
<div class="grid gap-6 grid-cols-1 lg:grid-cols-3 sm:grid-cols-2">
|
||||
{posts.map(b => <BlogCard blog={b} />)}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
66
src/pages/categories/index.astro
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import MainSection from '@components/blocks/MainSection.astro'
|
||||
import BlogCategory from '@components/blog/BlogCategory.astro'
|
||||
import { timeago } from '../../support/time'
|
||||
import { SITE } from '../../config'
|
||||
|
||||
const postMap: Map<string, CollectionEntry<'posts'>[]> = (await getCollection('posts')).sort(
|
||||
(a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
).reduce((acc, obj) => {
|
||||
let posts = acc.get(obj.data.category)
|
||||
if (!posts) {
|
||||
posts = []
|
||||
}
|
||||
posts.push(obj)
|
||||
|
||||
acc.set(obj.data.category, posts)
|
||||
|
||||
return acc
|
||||
}, new Map<string, CollectionEntry<'posts'>[]>())
|
||||
|
||||
const categories = await getCollection('categories')
|
||||
const description = 'Для убобства поиска мы разбиваем наши истории на категории'
|
||||
const URL = Astro.url.href
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="All Categories"
|
||||
description={description}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'inLanguage': 'ru-RU',
|
||||
'@id': URL,
|
||||
'url': URL,
|
||||
'name': `All Categories - ${SITE.title}`,
|
||||
'description': description,
|
||||
'isPartOf': {
|
||||
'@type': 'WebSite',
|
||||
'url': SITE.url,
|
||||
'name': SITE.title,
|
||||
'description': SITE.description,
|
||||
},
|
||||
}}
|
||||
|
||||
>
|
||||
<MainSection title="Категории" subTitle={description} />
|
||||
|
||||
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:pt-10 lg:py-14 2xl:max-w-full">
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 items-center gap-6 md:gap-10">
|
||||
{
|
||||
categories.map(c => (
|
||||
<BlogCategory
|
||||
slug={c.slug}
|
||||
title={c.data.title}
|
||||
description={c.data.description}
|
||||
count={postMap.get(c.slug)?.length ?? 0}
|
||||
publishDate={timeago(postMap.get(c.slug)?.[0]?.data?.pubDate)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
27
src/pages/favicon.ico.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import path from 'node:path'
|
||||
import type { APIRoute } from 'astro'
|
||||
import sharp from 'sharp'
|
||||
import ico from 'sharp-ico'
|
||||
|
||||
const faviconSrc = path.resolve('src/images/icon.png')
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
// Resize the image to multiple sizes
|
||||
const sizes = [16, 32]
|
||||
|
||||
const buffers = await Promise.all(
|
||||
sizes.map(async (size) => {
|
||||
return await sharp(faviconSrc)
|
||||
.resize(size)
|
||||
.toFormat('png')
|
||||
.toBuffer()
|
||||
}),
|
||||
)
|
||||
|
||||
// Convert the image to an ICO file
|
||||
const icoBuffer = ico.encode(buffers)
|
||||
|
||||
return new Response(icoBuffer, {
|
||||
headers: { 'Content-Type': 'image/x-icon' },
|
||||
})
|
||||
}
|
||||
123
src/pages/friends.astro
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import MainSection from '@components/blocks/MainSection.astro'
|
||||
import { Friends } from '../config'
|
||||
|
||||
const friends = Friends
|
||||
---
|
||||
|
||||
<BaseLayout title="Godruoyi's fiends" description="Godruoyi and his friends">
|
||||
<MainSection title="Hey Friends" subTitle="Email me if you want to show your link here." />
|
||||
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 mb-10 2xl:max-w-full">
|
||||
<div class="grid gap-6 grid-cols-1 lg:grid-cols-3 sm:grid-cols-2">
|
||||
{
|
||||
friends.map(f => (
|
||||
<div class="flex flex-col justify-between rounded-xl p-4 md:p-6 bg-white dark:bg-neutral-900/30">
|
||||
<div>
|
||||
<div class="flex items-center gap-x-4">
|
||||
<img
|
||||
class="rounded-full size-20"
|
||||
src={f.avatar}
|
||||
alt="Image Description"
|
||||
/>
|
||||
|
||||
<div class="grow">
|
||||
<h3 class="font-medium text-gray-800 dark:text-neutral-200">
|
||||
{f.name}
|
||||
</h3>
|
||||
<p class="text-xs mt-0.5 uppercase text-gray-500 dark:text-neutral-500">
|
||||
{f.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-gray-500 dark:text-neutral-500">
|
||||
{f.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-x-1">
|
||||
{
|
||||
f.social?.blog && (
|
||||
<a
|
||||
class="shadow-md inline-flex justify-center items-center size-8 text-sm font-semibold rounded-lg border border-gray-300 text-gray-500 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-neutral-300 dark:border-neutral-800 dark:hover:bg-neutral-700"
|
||||
href={f.social.blog}
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="flex-shrink-0 size-3.5"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<line x1="21.17" x2="12" y1="8" y2="8" />
|
||||
<line x1="3.95" x2="8.54" y1="6.06" y2="14" />
|
||||
<line x1="10.88" x2="15.46" y1="21.94" y2="14" />
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
f.social?.twitter && (
|
||||
<a
|
||||
class="shadow-md inline-flex justify-center items-center size-8 text-sm font-semibold rounded-lg border border-gray-300 text-gray-500 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-neutral-300 dark:border-neutral-800 dark:hover:bg-neutral-700"
|
||||
href=`https://twitter.com/${f.social.twitter}`
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="flex-shrink-0 size-3.5"
|
||||
>
|
||||
<path
|
||||
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
f.social?.github && (
|
||||
<a
|
||||
class="shadow-md inline-flex justify-center items-center size-8 text-sm font-semibold rounded-lg border border-gray-300 text-gray-500 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-neutral-300 dark:border-neutral-800 dark:hover:bg-neutral-700"
|
||||
href=`https://github.com/${f.social.github}`
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
class="flex-shrink-0 size-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
|
||||
/>
|
||||
<path d="M9 18c-4.51 2-5-2-7-2" />
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
51
src/pages/index.astro
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import HeroSection from '@components/blocks/HeroSection.astro'
|
||||
import BlogInsight from '@components/blog/BlogInsight.astro'
|
||||
import FeaturesSection from '@components/blocks/FeaturesSection.astro'
|
||||
import HeroSectionAlt from '@components/blocks/HeroSectionAlt.astro'
|
||||
import blogHeroImg from '@images/hero-landing.png'
|
||||
|
||||
const posts: CollectionEntry<'posts'>[] = (await getCollection('posts')).sort(
|
||||
(a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
).slice(0, 3);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<HeroSection
|
||||
title=`Столярный цех, <span class="text-yellow-500 dark:text-yellow-400">СТУСЛА.РУ</span>`
|
||||
subTitle="Приветсвую и добро пожаловат на сайт нашего столярного цеха, мы может вам сделать мебель на заказ, встроенные кухни, постоить каркасное строение и многое другое! Не стесняйтесь спрашивать нас! "
|
||||
primaryBtn="Написать в What's APP"
|
||||
primaryBtnURL="https://wa.me/79940180072?text=Здравствуйте,%20интересует%20ваш%20цех."
|
||||
src={blogHeroImg}
|
||||
alt="Написать в What's APP"
|
||||
/>
|
||||
|
||||
<FeaturesSection />
|
||||
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
|
||||
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
|
||||
<h2 class="text-3xl font-bold text-neutral-800 dark:text-neutral-200 md:text-4xl md:leading-tight">
|
||||
Последние посты
|
||||
</h2>
|
||||
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
|
||||
Истории наших работ за последнее время
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{
|
||||
posts.map(b => (
|
||||
<BlogInsight blog={b} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<HeroSectionAlt
|
||||
title="Написать в What's APP"
|
||||
subTitle="Пришлите ваши хотелки, мы используя свои перделки, реализуем их!"
|
||||
url="https://wa.me/79940180072?text=Здравствуйте,%20интересует%20ваш%20цех."
|
||||
/>
|
||||
</BaseLayout>
|
||||
137
src/pages/posts/[...slug].astro
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||
import { Image } from 'astro:assets'
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import { timeago } from '../../support/time'
|
||||
import { SITE } from '../../config'
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('posts')
|
||||
return posts.map(post => ({
|
||||
params: { slug: post.slug },
|
||||
props: post,
|
||||
}))
|
||||
}
|
||||
type Props = CollectionEntry<'posts'>
|
||||
|
||||
const post = Astro.props
|
||||
const { Content, remarkPluginFrontmatter } = await post.render()
|
||||
|
||||
const category: CollectionEntry<'categories'> = (await getCollection('categories')).filter(
|
||||
c => c.slug === post.data.category,
|
||||
).pop()
|
||||
|
||||
const tags = post.data.tags ?? [category.slug]
|
||||
const URL = Astro.url.href
|
||||
const ogImage = undefined; // todo
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
ogImage={ogImage}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'NewsArticle',
|
||||
'@id': URL,
|
||||
'url': URL,
|
||||
'description': post.data.description,
|
||||
'image': [
|
||||
// post.data.cardImage,
|
||||
],
|
||||
'headline': post.data.title,
|
||||
'datePublished': post.data.pubDate,
|
||||
'dateModified': post.data.pubDate,
|
||||
'author': [{
|
||||
'@type': 'Person',
|
||||
'name': SITE.author,
|
||||
'url': SITE.url,
|
||||
}],
|
||||
}}
|
||||
>
|
||||
<section class="mx-auto max-w-6xl px-4 pb-12 pt-8 sm:px-6 lg:px-8 lg:pt-12">
|
||||
<div class="shadow-none sm:shadow-sm mt-4 sm:mt-0">
|
||||
<div class="max-w-6xl">
|
||||
<Image
|
||||
class="w-full object-cover rounded-tl-sm rounded-tr-sm"
|
||||
src={post.data.cardImage}
|
||||
alt={post.data.title}
|
||||
draggable="false"
|
||||
format="avif"
|
||||
/>
|
||||
</div>
|
||||
<div class="sm:dark:bg-neutral-900/30 sm:bg-neutral-100 px-0 sm:px-6 md:px-10 lg:px-14 py-6">
|
||||
<div class="mb-10">
|
||||
<h2 class="block text-balance text-3xl font-bold tracking-tight text-neutral-800 dark:text-neutral-300 md:text-4xl lg:text-5xl">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<ol class="flex items-center whitespace-nowrap mt-2">
|
||||
<li class="inline-flex items-center">
|
||||
<a
|
||||
class="flex items-center text-sm text-orange-400 hover:text-orange-500 focus:outline-none focus:text-orange-500"
|
||||
href=`/categories/${category.slug}`
|
||||
>
|
||||
{category?.data?.title}
|
||||
</a>
|
||||
<svg
|
||||
class="flex-shrink-0 size-5 text-neutral-500 dark:text-neutral-600 mx-2"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="inline-flex items-center text-sm text-neutral-500 hover:text-neutral-600 focus:outline-none focus:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-400 dark:focus:text-neutral-400">
|
||||
{timeago(post.data.pubDate)}
|
||||
<svg
|
||||
class="flex-shrink-0 size-5 text-neutral-500 dark:text-neutral-600 mx-2"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li
|
||||
class="inline-flex items-center text-sm text-neutral-500 hover:text-neutral-600 focus:outline-none focus:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-400 dark:focus:text-neutral-400"
|
||||
aria-current="page"
|
||||
>
|
||||
{remarkPluginFrontmatter.minutesRead}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<article class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<div class="mt-10 md:mt-14 mx-auto grid max-w-screen-lg gap-y-5 sm:flex sm:items-center sm:justify-between sm:gap-y-0">
|
||||
<div class="flex flex-wrap gap-x-2 gap-y-1 sm:flex-nowrap sm:items-center sm:gap-y-0">
|
||||
{
|
||||
tags.map((tag: string) => (
|
||||
<span class="inline-flex items-center gap-x-1.5 rounded-lg bg-neutral-400/30 px-3 py-1.5 text-xs font-medium text-neutral-700 outline-none focus:outline-none focus-visible:outline-none focus-visible:ring dark:bg-neutral-700/60 dark:text-neutral-300">
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<!--<div class="flex items-center justify-end gap-x-1.5">-->
|
||||
<!-- <Bookmark />-->
|
||||
<!-- <div class="mx-3 block h-4 border-e border-neutral-400 dark:border-neutral-500"></div>-->
|
||||
<!-- <div class="inline-flex">-->
|
||||
<!-- <SocialShare pageTitle={post.data.title} />-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
49
src/pages/posts/index.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import MainSection from '@components/blocks/MainSection.astro'
|
||||
import BlogRecentCard from '@components/blog/BlogRecentCard.astro'
|
||||
import BlogSelectedArticle from '@components/blog/BlogSelectedArticle.astro'
|
||||
import { SITE } from '../../config'
|
||||
|
||||
const posts: CollectionEntry<'posts'>[] = (await getCollection('posts')).sort(
|
||||
(a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
)
|
||||
|
||||
const selectedPosts: CollectionEntry<'posts'>[] = posts.filter(p => p.data.selected)
|
||||
const description = 'Тут мы рассказывам о наших работах и история что случились с нами в процессе'
|
||||
const URL = Astro.url.href
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="All Blogs"
|
||||
description={description}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'inLanguage': 'ru-RU',
|
||||
'@id': URL,
|
||||
'url': URL,
|
||||
'name': `All Blogs - ${SITE.title}`,
|
||||
'description': description,
|
||||
'isPartOf': {
|
||||
'@type': 'WebSite',
|
||||
'url': SITE.url,
|
||||
'name': SITE.title,
|
||||
'description': SITE.description,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MainSection
|
||||
title="Обо всем по порядку"
|
||||
subTitle="Каждый наш заказ это своя история свой квест, и нам приятно поделиться с вами этими событиями"
|
||||
btnExists={true}
|
||||
btnTitle="Все посты"
|
||||
btnURL="/timeline"
|
||||
/>
|
||||
|
||||
<BlogRecentCard blogs={posts} />
|
||||
<BlogSelectedArticle posts={selectedPosts} />
|
||||
</BaseLayout>
|
||||
|
||||
31
src/pages/robots.txt.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// https://docs.astro.build/en/guides/integrations-guide/sitemap/#usage
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
const robotsTxt = `
|
||||
User-agent: Googlebot
|
||||
Disallow:
|
||||
Allow: /
|
||||
Crawl-delay: 10
|
||||
|
||||
User-agent: Yandex
|
||||
Disallow:
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: archive.org_bot
|
||||
Disallow:
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}`.trim()
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
return new Response(robotsTxt, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
},
|
||||
})
|
||||
}
|
||||
20
src/pages/rss.xml.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import rss from '@astrojs/rss'
|
||||
import { getCollection } from 'astro:content'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { SITE } from '../config.ts'
|
||||
|
||||
export async function GET() {
|
||||
const posts: CollectionEntry<'posts'>[] = (await getCollection('posts')).sort(
|
||||
(a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
)
|
||||
|
||||
return rss({
|
||||
title: SITE.title,
|
||||
description: SITE.description,
|
||||
site: SITE.url,
|
||||
items: posts.map(post => ({
|
||||
...post.data,
|
||||
link: `/posts/${post.slug}/`,
|
||||
})),
|
||||
})
|
||||
}
|
||||
43
src/pages/timeline.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { getCollection } from 'astro:content'
|
||||
import BaseLayout from '@layouts/BaseLayout.astro'
|
||||
import BlogTimeline from '@components/blog/BlogTimeline.astro'
|
||||
import { SITE } from '../config'
|
||||
|
||||
const posts: CollectionEntry<'posts'>[] = (await getCollection('posts')).sort(
|
||||
(a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
)
|
||||
const description = `Тут представлены все ${posts.length} наши статьи, за все время работы нашего цеха!`
|
||||
const URL = Astro.url.href
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Timeline"
|
||||
description={description}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'inLanguage': 'ru-RU',
|
||||
'@id': URL,
|
||||
'url': URL,
|
||||
'name': `Timeline - ${SITE.title}`,
|
||||
'description': description,
|
||||
'isPartOf': {
|
||||
'@type': 'WebSite',
|
||||
'url': SITE.url,
|
||||
'name': SITE.title,
|
||||
'description': SITE.description,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<section class="mx-auto max-w-[85rem] mt-10 lg:mt-10 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
|
||||
<div class="max-w-3xl mx-auto mb-10 lg:mb-14">
|
||||
<h2 class="text-3xl font-bold text-neutral-800 dark:text-neutral-200 md:text-4xl md:leading-tight">Timeline</h2>
|
||||
<p class="mt-4 text-lg group text-pretty text-neutral-600 dark:text-neutral-400">
|
||||
Всего <span class="group-hover:text-yellow-500 group-hover:dark:text-yellow-400">{posts.length}</span> За все нашу деятельность 💪
|
||||
</p>
|
||||
</div>
|
||||
{posts.map(p => <BlogTimeline blog={p} />)}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
67
src/support/time.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { format, register } from 'timeago.js'
|
||||
import getReadingTime from 'reading-time'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
|
||||
const TimeAgoConfiguration: string[][] = [
|
||||
['today', 'today'],
|
||||
['%s seconds ago', 'in %s seconds'],
|
||||
['1 minute ago', 'in 1 minute'],
|
||||
['%s minutes ago', 'in %s minutes'],
|
||||
['1 hour ago', 'in 1 hour'],
|
||||
['%s hours ago', 'in %s hours'],
|
||||
['1 day ago', 'in 1 day'],
|
||||
['%s days ago', 'in %s days'],
|
||||
['1 week ago', 'in 1 week'],
|
||||
['%s weeks ago', 'in %s weeks'],
|
||||
['1 month ago', 'in 1 month'],
|
||||
['%s months ago', 'in %s months'],
|
||||
['1 year ago', 'in 1 year'],
|
||||
['%s years ago', 'in %s years'],
|
||||
]
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
return `${year}/${month}/${day}`
|
||||
}
|
||||
|
||||
function formatDateFull(date: Date): string {
|
||||
const yyyy = date.getFullYear()
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
const hh = String(date.getHours()).padStart(2, '0')
|
||||
const mi = String(date.getMinutes()).padStart(2, '0')
|
||||
const ss = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
||||
}
|
||||
|
||||
function timeago(date?: Date): string {
|
||||
if (!date) {
|
||||
return 'today'
|
||||
}
|
||||
|
||||
const localeFunc = (number: number, index: number, _?: number): [string, string] => {
|
||||
return TimeAgoConfiguration[index] as [string, string]
|
||||
}
|
||||
|
||||
register('timeago', localeFunc)
|
||||
|
||||
return format(date, 'timeago')
|
||||
}
|
||||
|
||||
function remarkReadingTime() {
|
||||
// eslint-disable-next-line ts/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
return function (tree, { data }) {
|
||||
const textOnPage = toString(tree)
|
||||
const readingTime = getReadingTime(textOnPage)
|
||||
|
||||
data.astro.frontmatter.minutesRead = readingTime.text
|
||||
}
|
||||
}
|
||||
|
||||
export { formatDate, timeago, formatDateFull, remarkReadingTime }
|
||||
31
src/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface Site {
|
||||
title: string
|
||||
author: string
|
||||
url: string
|
||||
description: string
|
||||
shortDescription: string
|
||||
}
|
||||
|
||||
export interface NavigationLink {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface PickUpPost {
|
||||
title: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export interface Social {
|
||||
twitter?: string
|
||||
blog?: string
|
||||
github?: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
avatar: string
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
social: Social
|
||||
}
|
||||
122
tailwind.config.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
|
||||
'./node_modules/preline/preline.js',
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
gray: {
|
||||
100: '#f3f4f6',
|
||||
300: '#d1d5db',
|
||||
500: '#6b7280',
|
||||
600: '#4b5563',
|
||||
800: '#1f2937',
|
||||
},
|
||||
indigo: {
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
},
|
||||
neutral: {
|
||||
50: '#fafafa',
|
||||
100: '#f5f5f5',
|
||||
200: '#e5e5e5',
|
||||
300: '#d4d4d4',
|
||||
350: '#bfbfbf',
|
||||
400: '#a3a3a3',
|
||||
500: '#737373',
|
||||
600: '#525252',
|
||||
700: '#404040',
|
||||
800: '#262626',
|
||||
900: '#171717',
|
||||
},
|
||||
yellow: {
|
||||
50: '#fefce8',
|
||||
100: '#fef9c3',
|
||||
400: '#facc15',
|
||||
500: '#eab308',
|
||||
},
|
||||
orange: {
|
||||
100: '#ffedd5',
|
||||
200: '#fed7aa',
|
||||
300: '#fb713b',
|
||||
400: '#fa5a15',
|
||||
500: '#e14d0b',
|
||||
600: '#ea580c',
|
||||
},
|
||||
red: {
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
},
|
||||
zinc: {
|
||||
200: '#e4e4e7',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
900: '#18181b',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
typography: ({ theme }) => ({
|
||||
blog: {
|
||||
css: {
|
||||
'--tw-prose-body': theme('colors.neutral[700]'),
|
||||
'--tw-prose-headings': theme('colors.neutral[900]'),
|
||||
'--tw-prose-lead': theme('colors.neutral[700]'),
|
||||
'--tw-prose-links': theme('colors.orange[300]'),
|
||||
'--tw-prose-bold': theme('colors.neutral[900]'),
|
||||
'--tw-prose-counters': theme('colors.neutral[600]'),
|
||||
'--tw-prose-bullets': theme('colors.neutral[400]'),
|
||||
'--tw-prose-hr': theme('colors.neutral[300]'),
|
||||
'--tw-prose-quotes': theme('colors.neutral[500]'),
|
||||
'--tw-prose-quote-borders': theme('colors.neutral[300]'),
|
||||
'--tw-prose-captions': theme('colors.neutral[700]'),
|
||||
'--tw-prose-code': theme('colors.neutral[700]'),
|
||||
'--tw-prose-pre-code': theme('colors.neutral[200]'),
|
||||
'--tw-prose-pre-bg': theme('colors.neutral[900]'),
|
||||
'--tw-prose-th-borders': theme('colors.neutral[300]'),
|
||||
'--tw-prose-td-borders': theme('colors.neutral[200]'),
|
||||
|
||||
'--tw-prose-invert-body': theme('colors.neutral[400]'),
|
||||
'--tw-prose-invert-headings': theme('colors.neutral[200]'),
|
||||
'--tw-prose-invert-lead': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-links': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-bold': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-counters': theme('colors.neutral[400]'),
|
||||
'--tw-prose-invert-bullets': theme('colors.neutral[600]'),
|
||||
'--tw-prose-invert-hr': theme('colors.neutral[700]'),
|
||||
'--tw-prose-invert-quotes': theme('colors.neutral[500]'),
|
||||
'--tw-prose-invert-quote-borders': theme('colors.neutral[500]'),
|
||||
'--tw-prose-invert-captions': theme('colors.neutral[400]'),
|
||||
'--tw-prose-invert-code': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-pre-code': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 50%)',
|
||||
'--tw-prose-invert-th-borders': theme('colors.neutral[600]'),
|
||||
'--tw-prose-invert-td-borders': theme('colors.neutral[700]'),
|
||||
},
|
||||
},
|
||||
DEFAULT: {
|
||||
css: {
|
||||
blockquote: {
|
||||
fontStyle: 'normal',
|
||||
quotes: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('tailwindcss/nesting'),
|
||||
require('preline/plugin'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@content/*": ["content/*"],
|
||||
"@layouts/*": ["layouts/*"],
|
||||
"@pages/*": ["pages/*"],
|
||||
"@images/*": ["images/*"],
|
||||
}
|
||||
}
|
||||
}
|
||||