Rewritten in Astro (i had better separate this commit into smaller ones)

This commit is contained in:
DarkCat09 2023-05-05 14:01:09 +04:00
commit be964a8c49
32 changed files with 10954 additions and 0 deletions

44
src/components/Icon.astro Normal file
View file

@ -0,0 +1,44 @@
---
export interface Props {
code: string;
}
const { code } = Astro.props;
/* https://icons8.com/line-awesome */
---
<span class="icon">{code}</span>
<style lang="less">
@font-face {
font-family: 'Line Awesome Regular';
font-style: normal;
font-weight: 400;
font-display: auto;
src: url("/fonts/la-regular-400.eot?#iefix") format("embedded-opentype"), url("/fonts/la-regular-400.woff2") format("woff2"), url("/fonts/la-regular-400.woff") format("woff"), url("/fonts/la-regular-400.ttf") format("truetype");
}
@font-face {
font-family: 'Line Awesome Solid';
font-style: normal;
font-weight: 900;
font-display: auto;
src: url("/fonts/la-solid-900.eot?#iefix") format("embedded-opentype"), url("/fonts/la-solid-900.woff2") format("woff2"), url("/fonts/la-solid-900.woff") format("woff"), url("/fonts/la-solid-900.ttf") format("truetype");
}
@font-face {
font-family: 'Line Awesome Brands';
font-style: normal;
font-weight: normal;
font-display: auto;
src: url("/fonts/la-brands-400.eot?#iefix") format("embedded-opentype"), url("/fonts/la-brands-400.woff2") format("woff2"), url("/fonts/la-brands-400.woff") format("woff"), url("/fonts/la-brands-400.ttf") format("truetype");
}
span.icon {
display: inline-block;
font-family: 'Line Awesome Regular', 'Line Awesome Solid', 'Line Awesome Brands';
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
</style>

66
src/components/Menu.astro Normal file
View file

@ -0,0 +1,66 @@
---
import Icon from "./Icon.astro";
import ThemeModal from "./ThemeModal.astro";
---
<ThemeModal />
<nav>
<ul>
<li>
<a href="#">
<Icon code="&#xf015;" />
Homepage
</a>
</li>
<li>
<a href="#skills">
Skills
</a>
</li>
<li>
<a href="#projects">
Projects
</a>
</li>
</ul>
<ul>
<li>
<a href="#theme">
Theme
</a>
</li>
<li>
<a href="#dc09ru">
Services
</a>
</li>
<li>
<a href="https://blog.dc09.ru">
Blog
</a>
</li>
</ul>
</nav>
<style lang="less">
nav {
margin: 0.4rem;
display: flex;
flex-direction: row;
justify-content: space-between;
}
ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
li {
padding: 0 0.6rem;
font-size: 1.1rem;
}
</style>

View file

@ -0,0 +1,107 @@
---
import Icon from './Icon.astro';
export interface Props {
app: string;
url: string;
name: string;
icon: string;
copyName?: boolean;
}
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>
<script>
const copy = document.querySelectorAll('.copy')
const copyClickHandler = (ev: Event) => {
const elem = ev.currentTarget as HTMLElement
const link = elem.dataset.url
if (!link) return
navigator.clipboard.writeText(link)
}
for (let elem of copy) {
elem.addEventListener('click', copyClickHandler)
}
</script>
<style lang="less">
.profile {
padding: 0.4rem 0.6rem;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 2rem;
&:hover {
background: var(--accent-bg);
}
}
a.profile-main {
@icon-size: 2.3rem;
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
color: var(--fg);
text-decoration: none;
.icon-wrapper {
font-size: @icon-size;
margin-right: 0.4rem;
}
}
a.copy {
@icon-size: 1.8rem;
position: relative;
font-size: 1.1rem;
display: flex;
flex-direction: column;
align-items: center;
.icon-wrapper {
font-size: @icon-size;
margin-left: 0.4rem;
}
}
.content {
display: flex;
flex-direction: column;
.app {
font-weight: 600;
}
.nickname {
font-size: 0.9rem;
color: var(--fg-sec);
}
}
</style>

View file

@ -0,0 +1,73 @@
---
import Input from '../layouts/Input.astro';
export interface Props {
id: string;
label: string;
checked?: boolean;
}
const { id, label, checked = false } = Astro.props;
---
<Input label={label}>
<span class="input-wrapper" slot="before">
<input type="checkbox" name={id} id={id} checked={checked} />
<span class="switch">
<span class="circle"></span>
</span>
</span>
</Input>
<style lang="less">
input {
display: none;
}
@switch-width: 2.5rem;
@switch-height: @switch-width * 0.5;
@switch-bdrs: @switch-width * 0.4;
@circle-size: @switch-width * 0.4;
@circle-margin: @switch-width * 0.04;
.switch {
display: inline-flex;
flex-direction: row;
align-items: center;
width: @switch-width;
height: @switch-height;
margin-right: 0.5rem;
background: var(--bg-sec);
border-radius: @switch-bdrs;
.circle {
width: @circle-size;
height: @circle-size;
margin-right: 0;
margin-left: @circle-margin;
// on the left if not checked
transform: translateX(0rem);
transition: transform 0.25s ease 0s;
background: #fff;
border-radius: 50%;
}
}
input:checked ~ .switch {
background: var(--accent);
.circle {
margin-left: 0;
margin-right: @circle-margin;
// on the right if checked
@x: @switch-width - @circle-margin - @circle-size;
transform: translateX(@x);
}
}
</style>

View file

@ -0,0 +1,22 @@
---
export interface Props {
id: string;
label: string;
placeholder?: string;
regex?: string | null;
}
const { id, label, placeholder = "", regex = null } = Astro.props;
---
<label>
<span class="label">{label}</span>
<input type="text" name={id} id={id} placeholder={placeholder} pattern={regex}>
</label>
<style lang="less">
input {
background: var(--sec-bg);
color: var(--fg);
}
</style>

View file

@ -0,0 +1,164 @@
---
import Modal from "../layouts/Modal.astro";
import Switch from "./Switch.astro";
import TextBox from "./TextBox.astro";
---
<Modal id="theme">
<noscript>
These options will <b>not</b> work without JavaScript
</noscript>
<Switch id="dark" label="Dark theme" checked />
<Switch id="custom-theme" label="Set custom colors" />
<section id="custom-colors">
<TextBox id="accent" label="Accent color" placeholder="HEX (#fff)" regex="#?(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})" />
<label>
<span class="label">Accent color</span>
<input type="color" name="accent" id="accent">
</label>
</section>
</Modal>
<style lang="less">
#custom-colors {
display: none;
&[data-show=1] {
display: flex;
flex-direction: column;
}
}
</style>
<script>
const colorsSection = document.getElementById('custom-colors')
class ThemingOption {
private cookieKey: string
private cookieSwitch: Array<string | RegExp>
private callback: (input: HTMLInputElement, state: boolean) => any
private getval: (input: HTMLInputElement) => any
private media: string | null
private _input: HTMLInputElement | null
get input() { return this._input }
constructor (
cookieKey: string,
cookieSwitch: Array<string | RegExp> | null,
callback: (input: HTMLInputElement, state: boolean) => any,
getval: (input: HTMLInputElement) => any,
input: HTMLInputElement | null = null,
media: string | null = null,
) {
this.cookieKey = cookieKey
this.cookieSwitch = cookieSwitch
this.callback = callback
this.getval = getval
this._input = input
this.media = media
}
/*
function themeCheck() {
if (
document.cookie.includes('dc09_dark=1') || (
matchMedia('(prefers-color-scheme: dark)') &&
!document.cookie.includes('dc09_dark=0')
)
) {
document.body.dataset.dark = '1'
switchDark.checked = true
}
else {
document.body.dataset.dark = '0'
switchDark.checked = false
}
}
Here is the rewritten themeCheck:
*/
check() {
const match = this.cookieSwitch.map(
value => document.cookie.match(
`${this.cookieKey}=${value}`
)
)
if (
match[1] || (
this.media && !match[0] &&
matchMedia(this.media).matches
)
) {
if (this.callback)
this.callback(this._input, true)
return
}
if (this.callback)
this.callback(this._input, false)
}
update() {
const value = this.getval(this._input)
const localhost = window.location.hostname == 'localhost'
const domain = localhost ? 'localhost' : 'dc09.ru'
const cookie = `${this.cookieKey}=${value};domain=${domain};samesite=lax`
document.cookie = cookie
this.check()
}
}
const opts = {
dark: new ThemingOption(
'dc09_dark', ['0', '1'],
(input: HTMLInputElement, state: boolean) => {
input.checked = state
document.body.dataset.dark = state ? '1' : '0'
},
(input: HTMLInputElement) => input.checked ? 1 : 0,
document.getElementById('dark') as HTMLInputElement,
'(prefers-color-scheme: dark)',
),
custom: new ThemingOption(
'dc09_custom', ['0', '1'],
(input: HTMLInputElement, state: boolean) => {
input.checked = state
colorsSection.dataset.show = state ? '1' : '0'
if (state)
loadCustomTheme()
},
(input: HTMLInputElement) => input.checked ? 1 : 0,
document.getElementById('custom-theme') as HTMLInputElement,
)
}
for (let optObj of Object.values(opts)) {
((opt) => {
opt.check()
opt.input.addEventListener('input', () => opt.update())
})(optObj)
}
function loadCustomTheme() {
// Load `theme.less`
const lessStyles = document.createElement('link')
lessStyles.rel = 'stylesheet/less'
lessStyles.type = 'text/css'
lessStyles.href = '/src/styles/theme.less'
document.head.append(lessStyles)
// Load LessCSS script element
const lessScript = document.createElement('script')
lessScript.src = 'https://cdn.jsdelivr.net/npm/less'
// Replace variables
lessScript.onload = () => {
eval('less').modifyVars({
'accent': '#50aa70',
})
}
// Load LessCSS
document.head.append(lessScript)
}
</script>

1
src/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="astro/client" />

26
src/layouts/Input.astro Normal file
View file

@ -0,0 +1,26 @@
---
export interface Props {
label: string;
}
const { label } = Astro.props;
---
<label>
<slot name="before" />
<span class="label">{label}</span>
<slot name="after" />
</label>
<style lang="less">
label {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
&:not(:first-of-type) {
margin-top: 0.25rem;
}
}
</style>

73
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,73 @@
---
import Menu from '../components/Menu.astro';
export interface Props {
title: string;
}
const { title } = Astro.props;
---
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<meta name="description" content="Hi! I'm Andrew aka DarkCat09, a 13-years-old fullstack developer from Russia" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<Menu />
<slot />
</body>
</html>
<style lang="less" is:global>
@import "/src/styles/theme.less";
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Liberation Sans', 'Helvetica Neue', sans-serif;
display: flex;
flex-direction: column;
}
main {
margin: auto;
padding: 0.25rem;
max-width: 300rem;
}
section {
margin: 0.5rem 0;
}
hgroup > * {
margin: 0;
&:last-child {
margin: initial;
color: var(--fg-sec);
font-size: 1.2rem;
font-weight: 500;
}
}
h1 { font-size: 2.4rem; }
a, .link {
color: var(--accent);
text-decoration: none;
&:hover {
color: var(--accent-hl);
}
}
</style>

68
src/layouts/Modal.astro Normal file
View file

@ -0,0 +1,68 @@
---
import Icon from '../components/Icon.astro';
export interface Props {
id: string;
}
const { id } = Astro.props;
---
<div class="modal-bg" id={id}>
<a href="#" class="modal-bg-close"></a>
<div class="modal">
<div class="controls">
<a href="#"><Icon code="&#xf00d;" /></a>
</div>
<div class="content">
<slot />
</div>
</div>
</div>
<style lang="less">
.modal-bg {
display: none;
position: absolute;
z-index: 100;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
&:target {
display: flex;
flex-direction: column;
}
}
.modal-bg-close {
display: block;
position: absolute;
width: 100vw;
height: 100vh;
z-index: 101;
cursor: default;
}
.modal {
display: flex;
flex-direction: column;
margin: auto;
padding: 1rem;
z-index: 102;
border-radius: 2rem;
background: var(--bg);
}
.controls {
display: flex;
flex-direction: row;
justify-content: end;
}
</style>

36
src/pages/index.astro Normal file
View file

@ -0,0 +1,36 @@
---
import Layout from '../layouts/Layout.astro';
import Profile from '../components/Profile.astro';
---
<Layout title="DarkCat09 | Homepage">
<main>
<hgroup>
<h1>Hi! I'm Andrew.</h1>
<h2>13-years-old fullstack dev</h2>
</hgroup>
<section id="profiles">
<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>
</main>
</Layout>
<style lang="less">
#profiles {
display: grid;
grid-template-columns: repeat(1, auto);
@media (min-width: 450px) {
grid-template-columns: repeat(2, auto);
}
@media (min-width: 670px) {
grid-template-columns: repeat(3, auto);
}
}
</style>

42
src/styles/theme.less Normal file
View file

@ -0,0 +1,42 @@
@accent: #5070ca;
body {
@bg-light: #fff;
@fg-light: #202020;
@bg-dark: #222229;
@fg-dark: #eee;
--accent: @accent;
.light-mixin() {
--bg: @bg-light;
--fg: @fg-light;
--bg-sec: darken(@bg-light, 30%);
--fg-sec: lighten(@fg-light, 15%);
--accent-bg: darken(@bg-light, 10%);
--accent-hl: darken(@accent, 15%);
}
.dark-mixin() {
--bg: @bg-dark;
--fg: @fg-dark;
--bg-sec: lighten(@bg-dark, 15%);
--fg-sec: darken(@bg-light, 25%);
--accent-bg: lighten(@bg-dark, 5%);
--accent-hl: lighten(@accent, 15%);
}
.dark-mixin();
// &.dark {
&[data-dark="1"] {
.dark-mixin();
}
// &.light {
&[data-dark="0"] {
.light-mixin();
}
@media (prefers-color-scheme: light) {
.light-mixin();
}
}