Compare commits

..

5 commits

16 changed files with 618 additions and 481 deletions

View file

@ -30,4 +30,7 @@ export default defineConfig({
rehypeFigure,
],
},
redirects: {
"/blog": "/blog/page1",
}
});

675
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
"astro": "astro"
},
"dependencies": {
"astro": "^2.3.0",
"astro": "^2.9.0",
"astro-compress": "^1.1.42",
"less": "^4.1.3",
"reading-time": "^1.5.0",

View file

@ -0,0 +1,127 @@
---
export interface Props {
title: string;
text: string;
url: string;
elemClass?: Array<String>;
btnClass?: Array<string>;
dataJs?: string;
}
const {
title, text, url,
elemClass = [],
btnClass = [],
dataJs = "",
} = Astro.props;
---
<div class:list={["gi", ...elemClass]}>
<a href={url} target="_blank" class="gi-main">
<span class="icon-wrapper">
<slot name="icon-primary" />
</span>
<span class="content">
<span class="title">
<slot name="title">
{title}
</slot>
</span>
<span class="text">
<slot name="text">
{text}
</slot>
</span>
</span>
</a>
<a href="javascript:void(0)" class:list={["gi-btn", ...btnClass]} data-js={dataJs}>
<span class="icon-wrapper">
<slot name="icon-btn" />
</span>
</a>
</div>
<style lang="less">
@import "/src/styles/grid_calc.less";
.gi {
// vertical horizontal
padding: 0.4rem @gi-horiz-padding;
display: flex;
flex-direction: row;
align-items: center;
width: @gi-width;
min-height: 3rem;
border-radius: var(--bdrs);
&:hover {
background: var(--accent-bg);
}
}
a.gi-main {
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
color: var(--fg);
text-decoration: none;
.icon-wrapper {
width: @gi-icon-primary;
font-size: @gi-icon-primary;
margin-right: @gi-icon-margin;
& > :global(img) {
width: @gi-icon-primary;
}
}
}
a.gi-btn {
position: relative;
font-size: 1.1rem;
display: flex;
flex-direction: column;
align-items: center;
.icon-wrapper {
width: @gi-icon-btn;
font-size: @gi-icon-btn;
margin-left: @gi-icon-margin;
& > :global(img) {
width: @gi-icon-btn;
}
}
}
.icon-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
width: @gi-text-width;
overflow: hidden;
.title {
font-weight: 600;
}
.text {
font-size: 0.9rem;
color: var(--fg-sec);
}
}
</style>

View file

@ -0,0 +1,34 @@
---
import GridItem from "./GridItem.astro";
export interface Props {
name: string;
description: string;
url: string;
iconUrl?: string;
}
const { name, description, url, iconUrl = `${url}/favicon.ico` } = Astro.props;
---
<GridItem
title={name}
text={description}
url={url}
elemClass={["gi-service"]}
>
<img src={iconUrl} slot="icon-primary" />
</GridItem>
<style lang="less" is:global>
@import "/src/styles/grid_calc.less";
// Overwriting default GridItem's width
.gi-service {
width: @service-width;
.content {
width: @service-text-width;
}
}
</style>

View file

@ -1,4 +1,5 @@
---
import GridItem from './GridItem.astro';
import Icon from './Icon.astro';
export interface Props {
@ -12,28 +13,21 @@ export interface Props {
const { app, url, name, icon, copyName = false } = Astro.props;
---
<div class="profile">
<a href={url} target="_blank" class="profile-main">
<span class="icon-wrapper">
<Icon code={icon} />
</span>
<span class="content">
<span class="app">{app}</span>
<span class="nickname">{name}</span>
</span>
</a>
<a href="javascript:void(0)" class="copy" data-url={copyName ? name : url}>
<span class="icon-wrapper">
<Icon code="&#xf0c5;" />
</span>
</a>
</div>
<GridItem
title={app} text={name} url={url}
elemClass={["gi-profile"]}
btnClass={["profile-copy"]}
dataJs={copyName ? name : url}
>
<Icon code={icon} slot="icon-primary" />
<Icon code="&#xf0c5;" slot="icon-btn" />
</GridItem>
<script>
const copy = document.querySelectorAll('.copy')
const copy = document.querySelectorAll('.profile-copy')
const copyClickHandler = (ev: Event) => {
const elem = ev.currentTarget as HTMLElement
const link = elem.dataset.url
const link = elem.dataset.js
if (!link) return
navigator.clipboard.writeText(link)
}
@ -42,70 +36,14 @@ const { app, url, name, icon, copyName = false } = Astro.props;
}
</script>
<style lang="less">
@import "/src/styles/profile_calc.less";
.profile {
// vertical horizontal
padding: 0.4rem @profile-horiz-padding;
display: flex;
flex-direction: row;
align-items: center;
<style lang="less" is:global>
@import "/src/styles/grid_calc.less";
.gi-profile {
width: @profile-width;
border-radius: var(--bdrs);
&:hover {
background: var(--accent-bg);
}
}
a.profile-main {
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
color: var(--fg);
text-decoration: none;
.icon-wrapper {
font-size: @profile-icon-app;
margin-right: @profile-icon-margin;
}
}
a.copy {
position: relative;
font-size: 1.1rem;
display: flex;
flex-direction: column;
align-items: center;
.icon-wrapper {
font-size: @profile-icon-copy;
margin-left: @profile-icon-margin;
}
}
.content {
display: flex;
flex-direction: column;
width: @profile-text-width;
overflow: hidden;
.app {
font-weight: 600;
}
.nickname {
font-size: 0.9rem;
color: var(--fg-sec);
.content {
width: @profile-text-width;
}
}
</style>

View file

@ -247,6 +247,11 @@ import TextBox from "./TextBox.astro";
}
function reloadTheme() {
if (!lessLoaded) {
setTimeout(reloadTheme, 150)
lessLoaded = true
return
}
const accent = '#' + accentOpt.value
const bdrs = bdrsOpt.value
const purebg = purebgOpt.value

13
src/layouts/Grid.astro Normal file
View file

@ -0,0 +1,13 @@
<section>
<slot />
</section>
<style lang="less">
section {
margin: 0.4rem 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
</style>

View file

@ -48,16 +48,18 @@ const { title, description } = Astro.props;
margin: auto;
padding: @main-padding;
max-width: @max-width;
/*
width: 100%;
overflow-x: hidden;
*/
}
article {
max-width: 100vw;
width: fit-content;
margin: auto;
&.centered {
display: flex;
flex-direction: column;
align-items: center;
}
}
section {

View file

@ -15,15 +15,11 @@ export async function getStaticPaths() {
}));
}
const { slug } = Astro.params;
const { post } = Astro.props;
const {
PostContent, title, description,
readingTime, date, image,
} = await postRenderer(post);
const locale = 'ru-RU';
---
<BlogLayout title={title} description={description} article={true}>

View file

@ -4,8 +4,22 @@ import ArticleCard from "../../components/ArticleCard.astro";
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
export async function getStaticPaths({ paginate }) {
const POSTS_ON_PAGE = 15;
const posts = (await getCollection('blog'))
.sort( // sort by slug (which includes date), desc
(a, b) => (a.slug < b.slug) ? 1 : -1
)
.map( // convert to objects
p => { return { post: p } }
);
// use Astro's function
return paginate(posts, { pageSize: POSTS_ON_PAGE });
}
const { page } = Astro.props;
const description =
"DarkCat09's blog. " +
"Reading a QR code without a phone, " +
@ -15,8 +29,11 @@ const description =
---
<BlogLayout title="Homepage" description={description}>
<Fragment slot="metadata">
<meta name="robots" content="noindex, follow" />
</Fragment>
<div id="articles">
{posts.map(post => <ArticleCard post={post} />)}
{page.data.map(({ post }) => <ArticleCard post={post} />)}
</div>
</BlogLayout>

View file

@ -1,27 +1,28 @@
---
import MainLayout from '../layouts/MainLayout.astro';
import Grid from '../layouts/Grid.astro';
import Profile from '../components/Profile.astro';
import ListItem from '../components/ListItem.astro';
const description =
"Hi! I'm Andrew aka DarkCat09, " +
"a 13-years-old fullstack developer from Russia";
"a 14-years-old fullstack developer from Russia";
---
<MainLayout page="Homepage" description={description}>
<article class="centered">
<hgroup>
<h1>Hi! I'm Andrew.</h1>
<h2><span id="age">13</span>-years-old fullstack dev</h2>
<h2><span id="age">14</span>-years-old fullstack dev</h2>
</hgroup>
<section id="profiles">
<Grid>
<Profile app="Gitea" icon="&#xf1d3;" name="DarkCat09" url="https://git.dc09.ru/DarkCat09" />
<Profile app="GitHub" icon="&#xf09b;" name="DarkCat09" url="https://github.com/DarkCat09" />
<Profile app="E-mail" icon="&#xf0e0;" name="darkcat09@vivaldi.net" url="mailto:darkcat09@vivaldi.net" copyName />
<Profile app="Telegram" icon="&#xf2c6;" name="@darkcat09" url="https://t.me/darkcat09" />
<Profile app="Matrix" icon="&#xf4ad;" name="darkcat09:dc09.ru" url="https://matrix.to/#/@darkcat09:dc09.ru" copyName />
<Profile app="Discord" icon="&#xf392;" name="DarkCat09#5587" url="https://discord.com/app" copyName />
</section>
</Grid>
</article>
<article class="card card-dashed" id="about">
<ul>
@ -47,7 +48,7 @@ const description =
</ul>
</ListItem>
<ListItem icon="&#xf108;">
Favorite OS: Manjaro Linux
Favorite OS: Arch Linux
</ListItem>
<ListItem icon="">
<code>
@ -61,21 +62,6 @@ const description =
</MainLayout>
<style lang="less">
@import "/src/styles/profile_calc.less";
#profiles {
display: grid;
grid-template-columns: repeat(1, auto);
@media (min-width: @profile-width * 2) {
grid-template-columns: repeat(2, auto);
}
@media (min-width: @profile-width * 3) {
grid-template-columns: repeat(3, auto);
}
}
#about > ul {
list-style: none;
margin: 0;
@ -95,9 +81,14 @@ const description =
// in years
let age = ageMs / (1000 * 60 * 60 * 24 * 365.25)
// fallback to default value
// + rounding
age = (age < 13 ? 13 : Math.floor(age))
// if age < 14, date on user's PC is incorrect,
// so it's better to not update age.
document.getElementById('age').innerText = String(age)
// otherwise, update:
if (age >= 14) {
// rounding
age = Math.floor(age)
document.getElementById('age').innerText = String(age)
}
</script>

View file

@ -1,5 +1,7 @@
---
import MainLayout from "../layouts/MainLayout.astro";
import Grid from "../layouts/Grid.astro";
import HostedService from "../components/HostedService.astro";
const description =
"Status and description of services " +
@ -7,5 +9,14 @@ const description =
---
<MainLayout page="Services" description={description}>
// soon
<Grid>
<HostedService name="SearXNG" description="Privacy-respecting hackable metasearch engine" url="https://searx.dc09.ru" />
<HostedService name="Gitea" description="Painless self-hosted software development service" url="https://git.dc09.ru" />
<HostedService name="Piped" description="Privacy-friendly alternative YouTube frontend, API and proxy" url="https://yt.dc09.ru" />
<HostedService name="Invidious" description="An open source alternative frontend to YouTube" url="https://inv.dc09.ru" />
<HostedService name="Shlink" description="The definitive self-hosted URL shortener written in PHP" url="https://url.dc09.ru" />
<HostedService name="SFTPGo" description="Fully featured and highly configurable SFTP server" url="https://cloud.dc09.ru" iconUrl="https://cloud.dc09.ru/static/favicon.ico" />
<HostedService name="Element" description="A glossy Matrix collaboration client for the web" url="https://elem.dc09.ru" iconUrl="https://elem.dc09.ru/themes/element/img/logos/element-logo.svg" />
<HostedService name="Cinny" description="A matrix client with simple, elegant and secure interface" url="https://cinny.dc09.ru" iconUrl="https://cinny.dc09.ru/assets/favicon-e5e25737.ico" />
</Grid>
</MainLayout>

45
src/styles/grid_calc.less Normal file
View file

@ -0,0 +1,45 @@
@import "/src/styles/max_width.less";
@gi-icon-primary: 2.3rem;
@gi-icon-btn: 1.8rem;
@gi-text-width: 9.5rem;
@profile-text-width: 9rem;
@service-text-width: 14rem;
@gi-icon-margin: 0.4rem;
@gi-horiz-padding: 0.6rem;
// GridItem width without text
@gi-width-wo-text: @gi-horiz-padding + @gi-icon-primary + @gi-icon-margin + @gi-icon-margin + @gi-icon-btn + @gi-horiz-padding;
@gi-width: @gi-width-wo-text + @gi-text-width;
@profile-width: @gi-width-wo-text + @profile-text-width;
@service-width: @gi-width-wo-text + @service-text-width;
.grid-mixin(@custom-width) {
/*
display: grid;
grid-template-columns: repeat(1, auto);
@width-for-2: @custom-width * 2 + @all-padding;
@width-for-3: @custom-width * 3 + @all-padding;
& when (@width-for-2 < @max-width) {
@media (min-width: @width-for-2) {
grid-template-columns: repeat(2, auto);
}
}
& when (@width-for-3 < @max-width) {
@media (min-width: @width-for-3) {
grid-template-columns: repeat(3, auto);
}
}
*/
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}

View file

@ -1,3 +1,4 @@
@max-width: 800px;
@max-width: 50rem;
@body-padding: 0.5rem;
@main-padding: 0.25rem;
@all-padding: @body-padding * 2 + @main-padding * 2;

View file

@ -1,9 +0,0 @@
@profile-icon-app: 2.3rem;
@profile-icon-copy: 1.8rem;
@profile-text-width: 9.5rem;
@profile-icon-margin: 0.4rem;
@profile-horiz-padding: 0.6rem;
@profile-width: @profile-horiz-padding + @profile-icon-app + @profile-icon-margin + @profile-text-width + @profile-icon-margin + @profile-icon-copy + @profile-horiz-padding;