feat: implement barebone zola templates
This commit is contained in:
parent
9c20f5ed2e
commit
f99a9ae2ac
198 changed files with 2434 additions and 227991 deletions
9
_src/assets/icons/email.svg
Normal file
9
_src/assets/icons/email.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="64px" height="64px" viewBox="-6 -6 36.00 36.00" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0">
|
||||
<rect x="-6" y="-6" width="36.00" height="36.00" rx="18" fill="#c6a0f6" strokewidth="0"/>
|
||||
</g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M4 7.00005L10.2 11.65C11.2667 12.45 12.7333 12.45 13.8 11.65L20 7" stroke="#111111" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <rect x="3" y="5" width="18" height="14" rx="2" stroke="#111111" stroke-width="2" stroke-linecap="round"/> </g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 823 B |
7
_src/assets/icons/github.svg
Normal file
7
_src/assets/icons/github.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg fill="#c6a0f6" height="64px" width="64px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-143 145 512 512" xml:space="preserve">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <g> <path d="M177.1,398.4c-8.6-1.5-17.6-1.3-26.4-0.8c-20,1.1-40,3-60,1.2c-9.5-0.9-19.1-1.8-28.7-1.6c-17.4,0.3-32,6-40.6,22.6 c-4.3,8.2-5.3,17-5,26.1c0.7,25.6,11.8,40.2,36.2,47.5c19.6,5.8,39.7,6.6,59.9,6.2c7.5,0,15,0.4,22.5-0.1 c15.5-0.9,30.7-3.2,45.4-8.6c15.2-5.6,24.3-16.3,27.6-31.8c1.3-6,1.9-12.3,1.8-18.4C209.6,420,195.6,401.5,177.1,398.4z M78.7,466.1c-6.5,7.1-15.9,7.2-22.6,0.3c-4.9-5-7.7-12.7-7.7-22.3c0.2-6.5,2.1-13.6,7.7-19.3c6.7-6.9,16.1-6.8,22.6,0.2 C88.5,435.6,88.5,455.5,78.7,466.1z M169.6,466.6c-6.1,6.3-14.9,6.5-21.4,0.7c-11.2-10.2-11.2-32.9,0-43.2 c6.4-5.9,15.2-5.7,21.4,0.6c5.7,5.8,7.6,13.1,7.9,20.9C177.2,453.5,175.2,460.7,169.6,466.6z"/> <path d="M113,145c-141.4,0-256,114.6-256,256s114.6,256,256,256s256-114.6,256-256S254.4,145,113,145z M244.2,423.4 c-1.4,11.4-3.8,23.1-7.9,33.8c-12,30.7-36,47.6-67.8,52.7c-18.2,2.9-36.9,3-57.1,4.5c-18.1-1.6-38-1.8-57.3-5.2 c-37.4-6.6-62.8-32.8-70.2-70.3c-3.8-19.1-4.9-38.3,1-57.3c3.1-9.8,8.2-18.5,14.8-26.4c0.9-1,1.7-2.5,1.6-3.8 c-1.1-17.2,0.9-34.2,6-50.6c4.2-13.7,1.1-12.9,16.3-8.9c18.3,4.8,34.3,14.7,50,25c1.8,1.2,4.6,1.7,6.8,1.3 c22.2-3.4,44.3-3.6,66.5,0.3c1.6,0.3,3.7-0.3,5.2-1.2c13.5-8.8,27.4-16.7,42.6-22.2c5.5-2,11.3-3.3,16.9-5c2.5-0.7,3.6,0.2,4.5,2.6 c6.8,19,9.6,38.5,8.6,58.6c-0.1,1.1,0.5,2.6,1.2,3.5C243,374.6,247.3,398.2,244.2,423.4z"/> </g> </g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
11
_src/assets/icons/hamburger.svg
Normal file
11
_src/assets/icons/hamburger.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="40px" height="40px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="" stroke-width="0.00024000000000000003" transform="rotate(0)">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(1.1999999999999993,1.1999999999999993), scale(0.9)">
|
||||
<rect x="-2.4" y="-2.4" width="28.80" height="28.80" rx="2.88" fill="none" strokewidth="0"></rect>
|
||||
</g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.384"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M2 5.5C2 4.94772 2.44772 4.5 3 4.5H21C21.5523 4.5 22 4.94772 22 5.5V6.5C22 7.05228 21.5523 7.5 21 7.5H3C2.44772 7.5 2 7.05228 2 6.5V5.5Z" fill="#cad3f5"></path>
|
||||
<path d="M2 11.5C2 10.9477 2.44772 10.5 3 10.5H21C21.5523 10.5 22 10.9477 22 11.5V12.5C22 13.0523 21.5523 13.5 21 13.5H3C2.44772 13.5 2 13.0523 2 12.5V11.5Z" fill="#cad3f5"></path>
|
||||
<path d="M3 16.5C2.44772 16.5 2 16.9477 2 17.5V18.5C2 19.0523 2.44772 19.5 3 19.5H21C21.5523 19.5 22 19.0523 22 18.5V17.5C22 16.9477 21.5523 16.5 21 16.5H3Z" fill="#cad3f5"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
7
_src/assets/icons/linkedin.svg
Normal file
7
_src/assets/icons/linkedin.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg fill="#c6a0f6" height="64px" width="64px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-143 145 512 512" xml:space="preserve" stroke="#c6a0f6">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M113,145c-141.4,0-256,114.6-256,256s114.6,256,256,256s256-114.6,256-256S254.4,145,113,145z M41.4,508.1H-8.5V348.4h49.9 V508.1z M15.1,328.4h-0.4c-18.1,0-29.8-12.2-29.8-27.7c0-15.8,12.1-27.7,30.5-27.7c18.4,0,29.7,11.9,30.1,27.7 C45.6,316.1,33.9,328.4,15.1,328.4z M241,508.1h-56.6v-82.6c0-21.6-8.8-36.4-28.3-36.4c-14.9,0-23.2,10-27,19.6 c-1.4,3.4-1.2,8.2-1.2,13.1v86.3H71.8c0,0,0.7-146.4,0-159.7h56.1v25.1c3.3-11,21.2-26.6,49.8-26.6c35.5,0,63.3,23,63.3,72.4V508.1z "/> </g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
BIN
_src/assets/images/portrait.jpg
Normal file
BIN
_src/assets/images/portrait.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
_src/assets/images/section-bg-1.png
Normal file
BIN
_src/assets/images/section-bg-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
60
_src/assets/style/_animations.scss
Normal file
60
_src/assets/style/_animations.scss
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@use './mixins';
|
||||
|
||||
@include mixins.responsive using ($screen-size) {
|
||||
.anim-#{$screen-size}-none {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.anim-group-#{$screen-size}-none > * {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hover {
|
||||
from {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate(0, var(--anim-translation-value));
|
||||
}
|
||||
}
|
||||
|
||||
$anim-base-offset: -1s;
|
||||
|
||||
.anim-idle-hover {
|
||||
--anim-translation-value: var(--prj-spacing-1);
|
||||
--anim-offset: 0s;
|
||||
animation: hover 1.5s ease-in-out var(--anim-offset) infinite alternate;
|
||||
}
|
||||
|
||||
.anim-idle-hover-group {
|
||||
& > * {
|
||||
@extend .anim-idle-hover;
|
||||
}
|
||||
|
||||
@for $index from 1 through 20 {
|
||||
$anim-offset: $anim-base-offset * $index;
|
||||
& > :nth-child(#{$index}n) {
|
||||
--anim-offset: #{$anim-offset};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.anim-hover-zoom {
|
||||
transition: scale 0.2s;
|
||||
&:hover {
|
||||
scale: 1.05;
|
||||
}
|
||||
}
|
||||
|
||||
.anim-hover-translate {
|
||||
--anim-translation-value: -5px;
|
||||
--anim-shadow-color: var(--prj-accent-bg);
|
||||
transition: translate 0.2s;
|
||||
|
||||
&:hover {
|
||||
translate: var(--anim-translation-value) var(--anim-translation-value);
|
||||
box-shadow: calc(var(--anim-translation-value) * -1)
|
||||
calc(var(--anim-translation-value) * -1) 0px 0px var(--anim-shadow-color);
|
||||
}
|
||||
}
|
||||
17
_src/assets/style/_mixins.scss
Normal file
17
_src/assets/style/_mixins.scss
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
@use './variables' as *;
|
||||
|
||||
@mixin responsive {
|
||||
@each $size-name, $size in $screen-sizes {
|
||||
@media screen and (min-width: $size) {
|
||||
@content ($size-name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin responsive-steps($from, $to) {
|
||||
@include responsive using ($size-name) {
|
||||
@for $index from $from through $to {
|
||||
@content ($size-name, $index);
|
||||
}
|
||||
}
|
||||
}
|
||||
329
_src/assets/style/_utils.scss
Normal file
329
_src/assets/style/_utils.scss
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
@use './variables' as *;
|
||||
@use './mixins';
|
||||
|
||||
.position-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.position-sticky {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@include mixins.responsive using($screen-size) {
|
||||
.d-#{$screen-size}-none {
|
||||
display: none;
|
||||
}
|
||||
.d-#{$screen-size}-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-#{$screen-size}-flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-eq > * {
|
||||
flex: 100%;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
.flex-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
@include mixins.responsive using($screen-size) {
|
||||
.flex-#{$screen-size}-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
.flex-#{$screen-size}-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hstack {
|
||||
--prj-gap: var(--prj-spacing-3);
|
||||
display: flex;
|
||||
gap: var(--prj-gap);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hstack-reverse {
|
||||
--prj-gap: var(--prj-spacing-3);
|
||||
display: flex;
|
||||
gap: var(--prj-gap);
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.vstack {
|
||||
--prj-gap: var(--prj-spacing-3);
|
||||
display: flex;
|
||||
gap: var(--prj-gap);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vstack-reverse {
|
||||
--prj-gap: var(--prj-spacing-3);
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@include mixins.responsive using($size-name) {
|
||||
.flex-#{$size-name}-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-#{$size-name}-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.justify-content-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.justify-content-around {
|
||||
justify-content: space-around !important;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.grid {
|
||||
--prj-gap: var(--prj-spacing-3);
|
||||
--prj-columns: repeat(3, 1fr);
|
||||
--prj-min-col-width: 150px;
|
||||
display: grid;
|
||||
grid-template-columns: var(--prj-columns);
|
||||
gap: var(--prj-gap);
|
||||
}
|
||||
|
||||
@mixin grid-cols($amount) {
|
||||
--prj-columns: repeat(#{$amount}, minmax(var(--prj-min-col-width), 1fr));
|
||||
}
|
||||
|
||||
@for $i from 1 through 12 {
|
||||
.grid-cols-#{$i} {
|
||||
@include grid-cols($i);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.responsive-steps(1, 12) using ($size-name, $index) {
|
||||
.grid-#{$size-name}-cols-#{$index} {
|
||||
@include grid-cols($index);
|
||||
}
|
||||
}
|
||||
|
||||
.list-unstyle {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.text-justify {
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
}
|
||||
|
||||
.text-start {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-end {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.align-start {
|
||||
vertical-align: start;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.align-end {
|
||||
vertical-align: end;
|
||||
}
|
||||
|
||||
.overflow-scroll {
|
||||
overflow: scroll;
|
||||
}
|
||||
.overflow-x-scroll {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.overflow-y-scroll {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto;
|
||||
}
|
||||
.h-auto {
|
||||
height: auto;
|
||||
}
|
||||
@for $i from 0 through 100 {
|
||||
.w-#{$i} {
|
||||
width: percentage(calc($i / 100));
|
||||
}
|
||||
.h-#{$i} {
|
||||
height: percentage(calc($i / 100));
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.responsive-steps(0, 100) using ($size-name, $index) {
|
||||
.w-#{$size-name}-#{$index} {
|
||||
width: percentage(calc($index / 100));
|
||||
}
|
||||
|
||||
.h-#{$size-name}-#{$index} {
|
||||
height: percentage(calc($index / 100));
|
||||
}
|
||||
}
|
||||
|
||||
@mixin spacing-utils($name, $value, $screen-size: false) {
|
||||
@if $screen-size {
|
||||
$name: '#{$screen-size}-#{$name}';
|
||||
}
|
||||
|
||||
.m-#{$name} {
|
||||
margin: $value !important;
|
||||
}
|
||||
.mx-#{$name} {
|
||||
margin-left: $value !important;
|
||||
margin-right: $value !important;
|
||||
}
|
||||
.my-#{$name} {
|
||||
margin-top: $value !important;
|
||||
margin-bottom: $value !important;
|
||||
}
|
||||
.mt-#{$name} {
|
||||
margin-top: $value !important;
|
||||
}
|
||||
.mb-#{$name} {
|
||||
margin-bottom: $value !important;
|
||||
}
|
||||
.ml-#{$name} {
|
||||
margin-left: $value !important;
|
||||
}
|
||||
.mr-#{$name} {
|
||||
margin-right: $value !important;
|
||||
}
|
||||
.p-#{$name} {
|
||||
padding: $value !important;
|
||||
}
|
||||
.px-#{$name} {
|
||||
padding-left: $value !important;
|
||||
padding-right: $value !important;
|
||||
}
|
||||
.py-#{$name} {
|
||||
padding-top: $value !important;
|
||||
padding-bottom: $value !important;
|
||||
}
|
||||
.pt-#{$name} {
|
||||
padding-top: $value !important;
|
||||
}
|
||||
.pb-#{$name} {
|
||||
padding-bottom: $value !important;
|
||||
}
|
||||
.pl-#{$name} {
|
||||
padding-left: $value !important;
|
||||
}
|
||||
.pr-#{$name} {
|
||||
padding-right: $value !important;
|
||||
}
|
||||
|
||||
.gap-#{$name} {
|
||||
--prj-gap: #{$value};
|
||||
}
|
||||
}
|
||||
|
||||
@include spacing-utils(auto, auto);
|
||||
@include mixins.responsive using($screen-size) {
|
||||
@include spacing-utils(auto, auto);
|
||||
}
|
||||
|
||||
@each $index, $variable, $value in $spacings {
|
||||
@include spacing-utils($index, var(#{$variable}));
|
||||
@include mixins.responsive using($screen-size) {
|
||||
@include spacing-utils($index, var(#{$variable}), $screen-size);
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-0 {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.shadow-1 {
|
||||
box-shadow: 10px 10px 5px 0px var(--prj-shadow);
|
||||
}
|
||||
|
||||
.border-radius {
|
||||
border-radius: var(--prj-border-radius);
|
||||
}
|
||||
|
||||
.text-none {
|
||||
text-transform: none;
|
||||
}
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.text-lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
71
_src/assets/style/_variables.scss
Normal file
71
_src/assets/style/_variables.scss
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
@use 'sass:color';
|
||||
|
||||
/* Using catppuccin for now, make a theme switcher later */
|
||||
@use './themes/catppuccin/catppuccin';
|
||||
|
||||
@function getColor($color) {
|
||||
$ctp-theme: map-get(catppuccin.$palette, 'macchiato');
|
||||
@return map-get($ctp-theme, $color);
|
||||
}
|
||||
|
||||
/* Taken from Tailwind: https://tailwindcss.com/docs/container#using-the-container */
|
||||
$screen-sizes: (
|
||||
'sm': 640px,
|
||||
'md': 768px,
|
||||
'lg': 1024px,
|
||||
'xl': 1280px,
|
||||
'2xl': 1536px,
|
||||
);
|
||||
|
||||
$spacings: (
|
||||
0 '--prj-spacing-0' 0,
|
||||
1 '--prj-spacing-1' 0.25rem,
|
||||
2 '--prj-spacing-2' 0.5rem,
|
||||
3 '--prj-spacing-3' 1rem,
|
||||
4 '--prj-spacing-4' 2rem,
|
||||
5 '--prj-spacing-5' 3rem
|
||||
);
|
||||
|
||||
$border-radius: 0.5rem;
|
||||
|
||||
// Native CSS Variables to allow overridings and usage in external stylesheets
|
||||
:root {
|
||||
/* Variables prefixed with prj to avoid collisions */
|
||||
|
||||
/* Colors are inspired by Material Design: https://m2.material.io/design/color/the-color-system.html */
|
||||
--prj-bg: #{getColor('mantle')};
|
||||
--prj-bg-transparent: #{color.scale(getColor('mantle'), $alpha: -10%)};
|
||||
--prj-shadow: #{getColor('crust')};
|
||||
--prj-text: #{getColor('text')};
|
||||
|
||||
--prj-surface-1: #{getColor('base')};
|
||||
--prj-surface-2: #{darken(getColor('surface0'), 2%)};
|
||||
--prj-surface-3: #{getColor('surface1')};
|
||||
--prj-surface-text: #{getColor('text')};
|
||||
|
||||
--prj-link-text: #{getColor('teal')};
|
||||
|
||||
--prj-accent-bg: #{getColor('teal')};
|
||||
--prj-accent-text: #{getColor('base')};
|
||||
|
||||
--prj-primary: #{getColor('teal')};
|
||||
--prj-primary-text: #{getColor('base')};
|
||||
|
||||
--prj-secondary: #{getColor('mauve')};
|
||||
--prj-secondary-text: #{getColor('base')};
|
||||
|
||||
--prj-danger: #{getColor('red')};
|
||||
--prj-danger-text: #{getColor('base')};
|
||||
|
||||
--prj-disabled: #{getColor('red')};
|
||||
--prj-disabled-text: rgba(#{getColor('raw')}, 0.5);
|
||||
|
||||
--prj-input: #{getColor('text')};
|
||||
--prj-input-text: #{getColor('base')};
|
||||
|
||||
@each $index, $variable, $value in $spacings {
|
||||
#{$variable}: #{$value};
|
||||
}
|
||||
|
||||
--prj-border-radius: #{$border-radius};
|
||||
}
|
||||
176
_src/assets/style/style.scss
Normal file
176
_src/assets/style/style.scss
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
@use './variables.scss' as *;
|
||||
@use './utils.scss';
|
||||
@use './animations.scss';
|
||||
|
||||
// SASS variables are imported without namespace, but try to always use native
|
||||
// CSS variables when possible so they can be overrwritten by custom styles
|
||||
|
||||
html {
|
||||
background-color: var(--prj-bg);
|
||||
color: var(--prj-text);
|
||||
|
||||
/* Update font size based on screen width, source: https://matthewjamestaylor.com/responsive-font-size */
|
||||
font-size: calc(15px + 0.390625vw);
|
||||
}
|
||||
|
||||
body > main {
|
||||
max-width: 95vw; /* leave some space in the end by default */
|
||||
margin: auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
section:not(.clean) {
|
||||
/* outline: 1px solid var(--prj-accent-bg); */
|
||||
padding: var(--prj-spacing-3);
|
||||
background-color: var(--prj-surface-1);
|
||||
box-shadow: 10px 10px 5px 0px var(--prj-shadow);
|
||||
border-radius: var(--prj-border-radius);
|
||||
}
|
||||
|
||||
section:not(:first-of-type) {
|
||||
margin-top: var(--prj-spacing-4);
|
||||
}
|
||||
|
||||
h1,
|
||||
.fs-1 {
|
||||
font-size: 3rem !important;
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--prj-spacing-3);
|
||||
}
|
||||
h2,
|
||||
.fs-2 {
|
||||
font-size: 2.5rem !important;
|
||||
margin-bottom: var(--prj-spacing-3);
|
||||
}
|
||||
h3,
|
||||
.fs-3 {
|
||||
font-size: 2rem !important;
|
||||
margin-bottom: var(--prj-spacing-2);
|
||||
}
|
||||
h4,
|
||||
.fs-4 {
|
||||
font-size: 1.5rem !important;
|
||||
margin-bottom: var(--prj-spacing-2);
|
||||
}
|
||||
h5,
|
||||
.fs-5 {
|
||||
font-size: 1.25rem !important;
|
||||
margin-bottom: var(--prj-spacing-1);
|
||||
}
|
||||
h6,
|
||||
.fs-6 {
|
||||
font-size: 1rem !important;
|
||||
margin-bottom: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: var(--prj-spacing-2);
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Main content fix width, taken from Tailwind: https://tailwindcss.com/docs/container#using-the-container */
|
||||
@each $name, $size in $screen-sizes {
|
||||
@media screen and (min-width: $size) {
|
||||
body > main {
|
||||
max-width: $size;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
max-width: $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--prj-link-text);
|
||||
}
|
||||
|
||||
ul {
|
||||
/* Make the marker position is inside the container */
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
|
||||
ul {
|
||||
margin-left: var(--prj-spacing-3);
|
||||
}
|
||||
}
|
||||
|
||||
.list-unstyle {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
img.respect-width,
|
||||
video.respect-width {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
img.respect-height,
|
||||
video.respect-height {
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
li:not(:last-child) {
|
||||
margin-bottom: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--prj--primary-bg);
|
||||
color: var(--prj--primary-text);
|
||||
}
|
||||
|
||||
/* Lightgallery iframe fix */
|
||||
.lg-has-iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.lg-object {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
|
||||
var(--bg-image);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
color: var(--prj-text);
|
||||
|
||||
padding: var(--prj-spacing-3);
|
||||
|
||||
.text {
|
||||
padding: var(--prj-spacing-2);
|
||||
background-color: var(--prj-bg-transparent);
|
||||
|
||||
border-radius: var(--prj-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
transition: text-shadow 0.2s;
|
||||
--anim-shadow-color: var(--prj-accent-bg);
|
||||
|
||||
&:not(.clean):hover {
|
||||
text-shadow: 1px 1px 8px var(--anim-shadow-color);
|
||||
}
|
||||
}
|
||||
114
_src/assets/style/themes/catppuccin/_catppuccin.scss
Normal file
114
_src/assets/style/themes/catppuccin/_catppuccin.scss
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
$palette: (
|
||||
'latte': (
|
||||
'rosewater': #dc8a78,
|
||||
'flamingo': #dd7878,
|
||||
'pink': #ea76cb,
|
||||
'mauve': #8839ef,
|
||||
'red': #d20f39,
|
||||
'maroon': #e64553,
|
||||
'peach': #fe640b,
|
||||
'yellow': #df8e1d,
|
||||
'green': #40a02b,
|
||||
'teal': #179299,
|
||||
'sky': #04a5e5,
|
||||
'sapphire': #209fb5,
|
||||
'blue': #1e66f5,
|
||||
'lavender': #7287fd,
|
||||
'text': #4c4f69,
|
||||
'subtext1': #5c5f77,
|
||||
'subtext0': #6c6f85,
|
||||
'overlay2': #7c7f93,
|
||||
'overlay1': #8c8fa1,
|
||||
'overlay0': #9ca0b0,
|
||||
'surface2': #acb0be,
|
||||
'surface1': #bcc0cc,
|
||||
'surface0': #ccd0da,
|
||||
'base': #eff1f5,
|
||||
'mantle': #e6e9ef,
|
||||
'crust': #dce0e8,
|
||||
),
|
||||
'frappe': (
|
||||
'rosewater': #f2d5cf,
|
||||
'flamingo': #eebebe,
|
||||
'pink': #f4b8e4,
|
||||
'mauve': #ca9ee6,
|
||||
'red': #e78284,
|
||||
'maroon': #ea999c,
|
||||
'peach': #ef9f76,
|
||||
'yellow': #e5c890,
|
||||
'green': #a6d189,
|
||||
'teal': #81c8be,
|
||||
'sky': #99d1db,
|
||||
'sapphire': #85c1dc,
|
||||
'blue': #8caaee,
|
||||
'lavender': #babbf1,
|
||||
'text': #c6d0f5,
|
||||
'subtext1': #b5bfe2,
|
||||
'subtext0': #a5adce,
|
||||
'overlay2': #949cbb,
|
||||
'overlay1': #838ba7,
|
||||
'overlay0': #737994,
|
||||
'surface2': #626880,
|
||||
'surface1': #51576d,
|
||||
'surface0': #414559,
|
||||
'base': #303446,
|
||||
'mantle': #292c3c,
|
||||
'crust': #232634,
|
||||
),
|
||||
'macchiato': (
|
||||
'rosewater': #f4dbd6,
|
||||
'flamingo': #f0c6c6,
|
||||
'pink': #f5bde6,
|
||||
'mauve': #c6a0f6,
|
||||
'red': #ed8796,
|
||||
'maroon': #ee99a0,
|
||||
'peach': #f5a97f,
|
||||
'yellow': #eed49f,
|
||||
'green': #a6da95,
|
||||
'teal': #8bd5ca,
|
||||
'sky': #91d7e3,
|
||||
'sapphire': #7dc4e4,
|
||||
'blue': #8aadf4,
|
||||
'lavender': #b7bdf8,
|
||||
'text': #cad3f5,
|
||||
'subtext1': #b8c0e0,
|
||||
'subtext0': #a5adcb,
|
||||
'overlay2': #939ab7,
|
||||
'overlay1': #8087a2,
|
||||
'overlay0': #6e738d,
|
||||
'surface2': #5b6078,
|
||||
'surface1': #494d64,
|
||||
'surface0': #363a4f,
|
||||
'base': #24273a,
|
||||
'mantle': #1e2030,
|
||||
'crust': #181926,
|
||||
),
|
||||
'mocha': (
|
||||
'rosewater': #f5e0dc,
|
||||
'flamingo': #f2cdcd,
|
||||
'pink': #f5c2e7,
|
||||
'mauve': #cba6f7,
|
||||
'red': #f38ba8,
|
||||
'maroon': #eba0ac,
|
||||
'peach': #fab387,
|
||||
'yellow': #f9e2af,
|
||||
'green': #a6e3a1,
|
||||
'teal': #94e2d5,
|
||||
'sky': #89dceb,
|
||||
'sapphire': #74c7ec,
|
||||
'blue': #89b4fa,
|
||||
'lavender': #b4befe,
|
||||
'text': #cdd6f4,
|
||||
'subtext1': #bac2de,
|
||||
'subtext0': #a6adc8,
|
||||
'overlay2': #9399b2,
|
||||
'overlay1': #7f849c,
|
||||
'overlay0': #6c7086,
|
||||
'surface2': #585b70,
|
||||
'surface1': #45475a,
|
||||
'surface0': #313244,
|
||||
'base': #1e1e2e,
|
||||
'mantle': #181825,
|
||||
'crust': #11111b,
|
||||
),
|
||||
);
|
||||
26
_src/assets/style/themes/catppuccin/_frappe.scss
Normal file
26
_src/assets/style/themes/catppuccin/_frappe.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
$rosewater: #f2d5cf;
|
||||
$flamingo: #eebebe;
|
||||
$pink: #f4b8e4;
|
||||
$mauve: #ca9ee6;
|
||||
$red: #e78284;
|
||||
$maroon: #ea999c;
|
||||
$peach: #ef9f76;
|
||||
$yellow: #e5c890;
|
||||
$green: #a6d189;
|
||||
$teal: #81c8be;
|
||||
$sky: #99d1db;
|
||||
$sapphire: #85c1dc;
|
||||
$blue: #8caaee;
|
||||
$lavender: #babbf1;
|
||||
$text: #c6d0f5;
|
||||
$subtext1: #b5bfe2;
|
||||
$subtext0: #a5adce;
|
||||
$overlay2: #949cbb;
|
||||
$overlay1: #838ba7;
|
||||
$overlay0: #737994;
|
||||
$surface2: #626880;
|
||||
$surface1: #51576d;
|
||||
$surface0: #414559;
|
||||
$base: #303446;
|
||||
$mantle: #292c3c;
|
||||
$crust: #232634;
|
||||
26
_src/assets/style/themes/catppuccin/_latte.scss
Normal file
26
_src/assets/style/themes/catppuccin/_latte.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
$rosewater: #dc8a78;
|
||||
$flamingo: #dd7878;
|
||||
$pink: #ea76cb;
|
||||
$mauve: #8839ef;
|
||||
$red: #d20f39;
|
||||
$maroon: #e64553;
|
||||
$peach: #fe640b;
|
||||
$yellow: #df8e1d;
|
||||
$green: #40a02b;
|
||||
$teal: #179299;
|
||||
$sky: #04a5e5;
|
||||
$sapphire: #209fb5;
|
||||
$blue: #1e66f5;
|
||||
$lavender: #7287fd;
|
||||
$text: #4c4f69;
|
||||
$subtext1: #5c5f77;
|
||||
$subtext0: #6c6f85;
|
||||
$overlay2: #7c7f93;
|
||||
$overlay1: #8c8fa1;
|
||||
$overlay0: #9ca0b0;
|
||||
$surface2: #acb0be;
|
||||
$surface1: #bcc0cc;
|
||||
$surface0: #ccd0da;
|
||||
$base: #eff1f5;
|
||||
$mantle: #e6e9ef;
|
||||
$crust: #dce0e8;
|
||||
26
_src/assets/style/themes/catppuccin/_macchiato.scss
Normal file
26
_src/assets/style/themes/catppuccin/_macchiato.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
$rosewater: #f4dbd6;
|
||||
$flamingo: #f0c6c6;
|
||||
$pink: #f5bde6;
|
||||
$mauve: #c6a0f6;
|
||||
$red: #ed8796;
|
||||
$maroon: #ee99a0;
|
||||
$peach: #f5a97f;
|
||||
$yellow: #eed49f;
|
||||
$green: #a6da95;
|
||||
$teal: #8bd5ca;
|
||||
$sky: #91d7e3;
|
||||
$sapphire: #7dc4e4;
|
||||
$blue: #8aadf4;
|
||||
$lavender: #b7bdf8;
|
||||
$text: #cad3f5;
|
||||
$subtext1: #b8c0e0;
|
||||
$subtext0: #a5adcb;
|
||||
$overlay2: #939ab7;
|
||||
$overlay1: #8087a2;
|
||||
$overlay0: #6e738d;
|
||||
$surface2: #5b6078;
|
||||
$surface1: #494d64;
|
||||
$surface0: #363a4f;
|
||||
$base: #24273a;
|
||||
$mantle: #1e2030;
|
||||
$crust: #181926;
|
||||
26
_src/assets/style/themes/catppuccin/_mocha.scss
Normal file
26
_src/assets/style/themes/catppuccin/_mocha.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
$rosewater: #f5e0dc;
|
||||
$flamingo: #f2cdcd;
|
||||
$pink: #f5c2e7;
|
||||
$mauve: #cba6f7;
|
||||
$red: #f38ba8;
|
||||
$maroon: #eba0ac;
|
||||
$peach: #fab387;
|
||||
$yellow: #f9e2af;
|
||||
$green: #a6e3a1;
|
||||
$teal: #94e2d5;
|
||||
$sky: #89dceb;
|
||||
$sapphire: #74c7ec;
|
||||
$blue: #89b4fa;
|
||||
$lavender: #b4befe;
|
||||
$text: #cdd6f4;
|
||||
$subtext1: #bac2de;
|
||||
$subtext0: #a6adc8;
|
||||
$overlay2: #9399b2;
|
||||
$overlay1: #7f849c;
|
||||
$overlay0: #6c7086;
|
||||
$surface2: #585b70;
|
||||
$surface1: #45475a;
|
||||
$surface0: #313244;
|
||||
$base: #1e1e2e;
|
||||
$mantle: #181825;
|
||||
$crust: #11111b;
|
||||
52
_src/components/Button/Button.astro
Normal file
52
_src/components/Button/Button.astro
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
interface Props {
|
||||
className?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const { className = '', href } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
href !== undefined ? (
|
||||
<a href={href} class:list={['clean', 'btn', className]}>
|
||||
<slot />
|
||||
</a>
|
||||
) : (
|
||||
<button class:list={className}>
|
||||
<slot />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
<style lang="scss">
|
||||
button,
|
||||
.btn {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
padding: var(--prj-spacing-1) var(--prj-spacing-3);
|
||||
background-color: var(--prj-accent-bg);
|
||||
color: var(--prj-accent-text);
|
||||
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--prj-accent-bg);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
margin-bottom: 0;
|
||||
box-shadow: 0 0 0px 0px var(--prj-accent-bg);
|
||||
|
||||
transition: color 0.2s, background-color 0.2s, translate 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
--anim-translation-value: -5px;
|
||||
background-color: transparent;
|
||||
color: var(--prj-text);
|
||||
translate: var(--anim-translation-value) var(--anim-translation-value);
|
||||
box-shadow: calc(var(--anim-translation-value) * -2)
|
||||
calc(var(--anim-translation-value) * -2) 0px 0px var(--prj-accent-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
_src/components/Card.astro
Normal file
57
_src/components/Card.astro
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
export interface Props {
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const { className } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={['card', 'vstack', className]}>
|
||||
<div class="img-header">
|
||||
<slot name="img-header" />
|
||||
</div>
|
||||
<div class="title">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
|
||||
<div class="content flex-grow">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
background-color: var(--prj-surface-2);
|
||||
color: var(--prj-surface-text);
|
||||
border: 1px solid var(--prj-surface-2);
|
||||
border-radius: var(--prj-border-radius);
|
||||
box-shadow: 5px 5px 5px 5px var(--prj-shadow);
|
||||
|
||||
padding: var(--prj-spacing-2) var(--prj-spacing-3);
|
||||
|
||||
:global(a) {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
:global(a):hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.img-header {
|
||||
:global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.title > :global(:last-child) {
|
||||
margin-bottom: var(--prj-spacing-2);
|
||||
}
|
||||
</style>
|
||||
49
_src/components/Carousel/Carousel.module.css
Normal file
49
_src/components/Carousel/Carousel.module.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
.carousel {
|
||||
height: 100%;
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.itemContent {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btnPrev,
|
||||
.btnNext {
|
||||
top: 50%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btnPrev {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.btnNext {
|
||||
right: 0;
|
||||
}
|
||||
66
_src/components/Carousel/Carousel.tsx
Normal file
66
_src/components/Carousel/Carousel.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import React, { Children, useEffect, useRef, useState } from 'react';
|
||||
import classes from './Carousel.module.css';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Carousel({ children }: Props): JSX.Element {
|
||||
const [activeItem, setActiveItem] = useState(0);
|
||||
const content = useRef<HTMLDivElement>(null);
|
||||
const maxItems = Children.count(children) - 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (content.current == null) return;
|
||||
|
||||
const offset = content.current.clientWidth * activeItem;
|
||||
// const offset = 100 * activeItem;
|
||||
//
|
||||
console.log(offset, activeItem, content.current.clientWidth);
|
||||
|
||||
content.current.style.transform = `translate3d(-${offset}px, 0px, 0px)`;
|
||||
}, [activeItem]);
|
||||
|
||||
const offsetActiveItem = (offset: number): void => {
|
||||
setActiveItem((prev) => {
|
||||
const newActiveItem = prev + offset;
|
||||
|
||||
// Wrap on end in both sides
|
||||
if (newActiveItem < 0) {
|
||||
return maxItems;
|
||||
}
|
||||
|
||||
if (newActiveItem > maxItems) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return newActiveItem;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.carousel}>
|
||||
<button
|
||||
className={classes.btnPrev}
|
||||
onClick={() => {
|
||||
offsetActiveItem(-1);
|
||||
}}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<div className={classes.container}>
|
||||
<div ref={content} className={classes.content}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={classes.btnNext}
|
||||
onClick={() => {
|
||||
offsetActiveItem(1);
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
_src/components/Carousel/CarouselItem.tsx
Normal file
15
_src/components/Carousel/CarouselItem.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import classes from './Carousel.module.css';
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export default function CarouselItem({ children }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={classes.item}>
|
||||
<div className={classes.itemContent}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
_src/components/Inputs/SelectInput.module.css
Normal file
69
_src/components/Inputs/SelectInput.module.css
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
.wrapper {
|
||||
--bg-color: var(--prj-input);
|
||||
--text-color: var(--prj-input-text);
|
||||
|
||||
position: relative;
|
||||
padding: var(--prj-spacing-1);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
|
||||
display: flex;
|
||||
gap: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: var(--prj-spacing-1);
|
||||
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.selectedItem {
|
||||
background-color: var(--prj-surface-3);
|
||||
color: var(--prj-text);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.selectedItem > * {
|
||||
padding: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
.deleteItem:hover {
|
||||
background-color: var(--prj-danger);
|
||||
}
|
||||
|
||||
.optionList {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 120%;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: var(--prj-spacing-1);
|
||||
text-align: start;
|
||||
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.optionItem {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
text-align: start;
|
||||
padding: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
.optionItem:disabled {
|
||||
color: var(--prj-disabled-text);
|
||||
}
|
||||
|
||||
.optionItem:not(:first-child) {
|
||||
margin-top: var(--prj-spacing-1);
|
||||
}
|
||||
|
||||
.optionItem:not(:disabled):hover {
|
||||
background-color: var(--prj-accent-bg);
|
||||
}
|
||||
142
_src/components/Inputs/SelectInput.tsx
Normal file
142
_src/components/Inputs/SelectInput.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import styles from './SelectInput.module.css';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onChange: (value: string | string[] | null) => void;
|
||||
options: Option[];
|
||||
isMultiple?: boolean;
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
export default function SelectInput({
|
||||
options,
|
||||
isMultiple = false,
|
||||
onChange,
|
||||
value = [],
|
||||
}: Props): JSX.Element {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [filteredOptions, setFilteredOptions] = useState<Props['options']>([]);
|
||||
const [isOptionsOpen, setIsOptionsOpen] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredOptions(options);
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.length === 0) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(isMultiple ? selected : selected[0]);
|
||||
}, [selected]);
|
||||
|
||||
const handleFocusInput = (): void => {
|
||||
setIsOptionsOpen(true);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleAddElement = (item: any): void => {
|
||||
setSelected((prev) => {
|
||||
if (isMultiple) {
|
||||
return [...prev, item];
|
||||
}
|
||||
return [item];
|
||||
});
|
||||
setIsOptionsOpen(false);
|
||||
|
||||
if (inputRef.current === null) return;
|
||||
|
||||
inputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleRemoveElement = (idx: number): void => {
|
||||
setIsOptionsOpen(false);
|
||||
setSelected((prev) => {
|
||||
prev.splice(idx, 1);
|
||||
|
||||
return [...prev];
|
||||
});
|
||||
};
|
||||
|
||||
const handleLooseFocus = (e: React.FocusEvent<HTMLDivElement>): void => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
// Not triggered when swapping focus between children
|
||||
setIsOptionsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterOptions = ({
|
||||
target,
|
||||
}: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (target.value === '') {
|
||||
setFilteredOptions(options);
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptions = options.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().search(target.value.toLowerCase()) !== -1,
|
||||
);
|
||||
setFilteredOptions(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} onBlur={handleLooseFocus}>
|
||||
<div
|
||||
className={styles.input}
|
||||
onClick={() => {
|
||||
handleFocusInput();
|
||||
}}
|
||||
>
|
||||
{selected.map((item, idx) => (
|
||||
<div className={styles.selectedItem + ' hstack'} key={idx}>
|
||||
<div>{item}</div>
|
||||
<div
|
||||
className={styles.deleteItem}
|
||||
onClick={() => {
|
||||
handleRemoveElement(idx);
|
||||
}}
|
||||
>
|
||||
{'X'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={styles.realInput}
|
||||
type="text"
|
||||
onChange={handleFilterOptions}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
setSelected([]);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
<div className={styles.optionList} hidden={!isOptionsOpen}>
|
||||
{filteredOptions.map((item, idx) => (
|
||||
<button
|
||||
className={styles.optionItem}
|
||||
key={idx}
|
||||
disabled={selected.includes(item.value)}
|
||||
onClick={() => {
|
||||
handleAddElement(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
_src/components/Inputs/Types.ts
Normal file
0
_src/components/Inputs/Types.ts
Normal file
16
_src/components/LangSelector.astro
Normal file
16
_src/components/LangSelector.astro
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
import { LanguageSelector } from 'astro-i18next/components';
|
||||
---
|
||||
|
||||
<LanguageSelector showFlag={false} class="selector" />
|
||||
|
||||
<style lang="scss">
|
||||
.selector {
|
||||
padding: var(--prj-spacing-1);
|
||||
border: 1px solid var(--prj-input);
|
||||
color: var(--prj-input-text);
|
||||
background-color: var(--prj-input);
|
||||
font-size: 0.9rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
31
_src/components/LocalizedMarkdown.astro
Normal file
31
_src/components/LocalizedMarkdown.astro
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
import i18next from 'i18next';
|
||||
import config from '../../astro-i18next.config.mjs';
|
||||
|
||||
export interface Props {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const { path } = Astro.props;
|
||||
|
||||
const pages = await Astro.glob('../../public/locales/**/*.md');
|
||||
let markdown;
|
||||
|
||||
markdown = pages.find((page) =>
|
||||
page.file.includes(`locales/${i18next.language}/${path}`),
|
||||
);
|
||||
|
||||
if (!markdown) {
|
||||
markdown = pages.find((page) =>
|
||||
page.file.includes(`locales/${config.defaultLocale}/${path}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (!markdown) {
|
||||
throw Error(`The file: "${path}" was not found.`);
|
||||
}
|
||||
|
||||
const { Content } = markdown;
|
||||
---
|
||||
|
||||
<Content />
|
||||
18
_src/components/MediaGallery/Gallery.module.css
Normal file
18
_src/components/MediaGallery/Gallery.module.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.thumbnailList {
|
||||
display: grid;
|
||||
overflow-x: scroll;
|
||||
|
||||
gap: var(--prj-spacing-3);
|
||||
padding-bottom: var(--prj-spacing-2);
|
||||
|
||||
grid-auto-columns: 25%;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.thumbnailItem {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thumbnailItem:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
29
_src/components/MediaGallery/Gallery.tsx
Normal file
29
_src/components/MediaGallery/Gallery.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { type Media } from './types';
|
||||
|
||||
import classes from './Gallery.module.css';
|
||||
import Carousel from '@components/Carousel/Carousel';
|
||||
import CarouselItem from '@components/Carousel/CarouselItem';
|
||||
|
||||
interface Props {
|
||||
items: Media[];
|
||||
height: number;
|
||||
}
|
||||
|
||||
export default function Gallery({ items, height = 500 }: Props): JSX.Element {
|
||||
return (
|
||||
<div style={{ height }}>
|
||||
<Carousel>
|
||||
{items.map((item, idx) => (
|
||||
<CarouselItem key={idx}>
|
||||
<img
|
||||
className="respect-height"
|
||||
src={item.thumbnail ?? item.url}
|
||||
alt={item.alt}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
_src/components/MediaGallery/types.ts
Normal file
12
_src/components/MediaGallery/types.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export enum MediaType {
|
||||
Image = 'image',
|
||||
Video = 'video',
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
type: MediaType;
|
||||
url: string;
|
||||
alt: string;
|
||||
mime?: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
124
_src/components/Navbar.astro
Normal file
124
_src/components/Navbar.astro
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
---
|
||||
import { localizePath } from 'astro-i18next';
|
||||
import OffCanvas from '@components/OffCanvas/OffCanvas.astro';
|
||||
import OffCanvasBtn from '@components/OffCanvas/OffCanvasBtn.astro';
|
||||
import LangSelector from '@components/LangSelector.astro';
|
||||
|
||||
const links = [
|
||||
{ href: localizePath('/'), text: 'Home' },
|
||||
{ href: localizePath('/blog'), text: 'Blog' },
|
||||
{ href: localizePath('/projects'), text: 'Projects' },
|
||||
{ href: localizePath('/curriculum'), text: 'Curriculum' },
|
||||
{ href: localizePath('/contact'), text: 'Contact' },
|
||||
];
|
||||
---
|
||||
|
||||
<div id="main-navbar" class="pt-1">
|
||||
<nav class="navbar navbar-desktop d-none d-lg-block container">
|
||||
<ul class="list-unstyle hstack">
|
||||
{
|
||||
links.map((link) => (
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href={link.href}>
|
||||
{link.text}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
<li class="nav-item">
|
||||
<LangSelector />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="text-end d-lg-none">
|
||||
<OffCanvasBtn />
|
||||
<OffCanvas>
|
||||
<nav class="navbar navbar-mobile">
|
||||
<ul class="list-unstyle text-start">
|
||||
{
|
||||
links.map((link) => (
|
||||
<li class="nav-item mb-3">
|
||||
<a class="nav-link" href={link.href}>
|
||||
{link.text}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
<li class="nav-item mb-3">
|
||||
<LangSelector />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</OffCanvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const setActiveLink = () => {
|
||||
const links =
|
||||
document.querySelectorAll<HTMLAnchorElement>(`#main-navbar a`);
|
||||
|
||||
links.forEach((link) => {
|
||||
if (link.pathname === '/' && location.pathname === '/') {
|
||||
link.classList.add('active');
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.pathname === '/' && location.pathname !== '/') {
|
||||
link.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
location.pathname.startsWith(link.pathname)
|
||||
? link.classList.add('active')
|
||||
: link.classList.remove('active');
|
||||
});
|
||||
};
|
||||
// Add active class to the current link
|
||||
document.addEventListener('astro:page-load', setActiveLink, { once: true });
|
||||
document.addEventListener('astro:after-swap', setActiveLink);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
nav {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-desktop ul {
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li > a {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
--boder-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
|
||||
transition: background-color 200ms, color 200ms;
|
||||
}
|
||||
|
||||
a.active {
|
||||
border: 1px solid var(--prj-accent-bg);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
--border-color: var(--prj-accent-bg);
|
||||
background-color: var(--prj-accent-bg);
|
||||
color: var(--prj-accent-text);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
</style>
|
||||
107
_src/components/OffCanvas/OffCanvas.astro
Normal file
107
_src/components/OffCanvas/OffCanvas.astro
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
const { isOpen } = Astro.props;
|
||||
---
|
||||
|
||||
<div id="mobile-nav" class="off-canvas">
|
||||
<div class="off-canvas-content" transition:persist>
|
||||
<button class="off-canvas-toggle" data-target="#mobile-nav">
|
||||
<svg
|
||||
width="40px"
|
||||
height="40px"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#ffffff"
|
||||
><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"
|
||||
><path
|
||||
fill="#cad3f5"
|
||||
d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z"
|
||||
></path></g
|
||||
></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="off-canvas-backdrop off-canvas-toggle" data-target="#mobile-nav">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.off-canvas {
|
||||
.off-canvas-content {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
z-index: 5;
|
||||
|
||||
background-color: var(--prj-bg);
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 100%;
|
||||
|
||||
padding: var(--prj-spacing-3);
|
||||
|
||||
transition: left 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
&.active .off-canvas-content {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.off-canvas-backdrop {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
z-index: 4;
|
||||
|
||||
background-color: rgba(0, 0, 0);
|
||||
opacity: 0;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 100%;
|
||||
|
||||
padding: var(--prj-spacing-3);
|
||||
|
||||
// Delay the left transition on remove so it's desn't appear to be sliding or to be not working
|
||||
transition: opacity 0.8s ease, left 0s linear 1s;
|
||||
}
|
||||
|
||||
&.active .off-canvas-backdrop {
|
||||
left: 0%;
|
||||
opacity: 40%;
|
||||
transition: opacity 0.8s ease, left 0s linear;
|
||||
}
|
||||
}
|
||||
|
||||
button.off-canvas-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
document
|
||||
.querySelectorAll<HTMLElement>('.off-canvas-toggle')
|
||||
.forEach((btn) =>
|
||||
btn.addEventListener('click', () => {
|
||||
const { target } = btn.dataset;
|
||||
|
||||
if (!target) return;
|
||||
|
||||
document.querySelector(target)?.classList.toggle('active');
|
||||
}),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
53
_src/components/OffCanvas/OffCanvasBtn.astro
Normal file
53
_src/components/OffCanvas/OffCanvasBtn.astro
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<button id="btn-toggle" class="off-canvas-toggle" data-target="#mobile-nav">
|
||||
<svg
|
||||
width="40px"
|
||||
height="40px"
|
||||
viewBox="-2.4 -2.4 28.80 28.80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke=""
|
||||
stroke-width="0.00024000000000000003"
|
||||
transform="rotate(0)"
|
||||
><g
|
||||
id="SVGRepo_bgCarrier"
|
||||
stroke-width="0"
|
||||
transform="translate(1.1999999999999993,1.1999999999999993), scale(0.9)"
|
||||
><rect
|
||||
x="-2.4"
|
||||
y="-2.4"
|
||||
width="28.80"
|
||||
height="28.80"
|
||||
rx="2.88"
|
||||
fill="none"
|
||||
strokewidth="0"></rect></g
|
||||
><g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke="#CCCCCC"
|
||||
stroke-width="0.384"></g><g id="SVGRepo_iconCarrier">
|
||||
<path
|
||||
d="M2 5.5C2 4.94772 2.44772 4.5 3 4.5H21C21.5523 4.5 22 4.94772 22 5.5V6.5C22 7.05228 21.5523 7.5 21 7.5H3C2.44772 7.5 2 7.05228 2 6.5V5.5Z"
|
||||
fill="#cad3f5"></path>
|
||||
<path
|
||||
d="M2 11.5C2 10.9477 2.44772 10.5 3 10.5H21C21.5523 10.5 22 10.9477 22 11.5V12.5C22 13.0523 21.5523 13.5 21 13.5H3C2.44772 13.5 2 13.0523 2 12.5V11.5Z"
|
||||
fill="#cad3f5"></path>
|
||||
<path
|
||||
d="M3 16.5C2.44772 16.5 2 16.9477 2 17.5V18.5C2 19.0523 2.44772 19.5 3 19.5H21C21.5523 19.5 22 19.0523 22 18.5V17.5C22 16.9477 21.5523 16.5 21 16.5H3Z"
|
||||
fill="#cad3f5"></path>
|
||||
</g></svg
|
||||
>
|
||||
|
||||
<span class="visually-hidden">Open sidebar</span>
|
||||
</button>
|
||||
|
||||
<style lang="scss">
|
||||
button.off-canvas-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
133
_src/components/Pagination.astro
Normal file
133
_src/components/Pagination.astro
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
---
|
||||
import { type Page } from 'astro';
|
||||
|
||||
interface Props {
|
||||
page: Page;
|
||||
paginationOffset?: number;
|
||||
urlPattern: string;
|
||||
}
|
||||
|
||||
const { page, urlPattern, paginationOffset = 3 } = Astro.props;
|
||||
|
||||
const pages = [];
|
||||
|
||||
const lowerEnd = Math.max(page.currentPage - paginationOffset, 1);
|
||||
const highEnd = Math.min(page.currentPage + paginationOffset, page.lastPage);
|
||||
|
||||
const generateUrl = (index: number) => {
|
||||
return urlPattern.replace('{}', index.toString());
|
||||
};
|
||||
|
||||
for (let index = lowerEnd; index <= highEnd; index++) {
|
||||
pages.push({
|
||||
index,
|
||||
url: generateUrl(index),
|
||||
});
|
||||
}
|
||||
---
|
||||
|
||||
<nav role="navigation" aria-label="Pagination" class="w-100 my-4">
|
||||
<ul class="list-unstyle hstack justify-content-center">
|
||||
{
|
||||
page.url.prev !== undefined && (
|
||||
<li>
|
||||
<a
|
||||
href={page.url.prev}
|
||||
class="prev-page"
|
||||
aria-label="Go to previous page"
|
||||
>
|
||||
« Prev
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}{
|
||||
lowerEnd !== 1 && (
|
||||
<>
|
||||
<li>
|
||||
<a
|
||||
href={generateUrl(1)}
|
||||
class="prev-page"
|
||||
aria-label={`Go to page 1`}
|
||||
>
|
||||
1
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="start-ellipsis">…</span>
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
pages.map((item) => (
|
||||
<li>
|
||||
<a
|
||||
class:list={[{ current: item.index === page.currentPage }]}
|
||||
href={item.url}
|
||||
aria-label={`Go to page ${item.index}`}
|
||||
>
|
||||
{item.index}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
{
|
||||
highEnd !== page.lastPage && (
|
||||
<>
|
||||
<li>
|
||||
<span class="start-ellipsis">…</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={generateUrl(page.lastPage)}
|
||||
class="next-page"
|
||||
aria-label={`Go to page ${page.lastPage}`}
|
||||
>
|
||||
{page.lastPage}
|
||||
</a>
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
page.url.next !== undefined && (
|
||||
<li>
|
||||
<a
|
||||
href={page.url.next}
|
||||
class="next-page"
|
||||
aria-label="Go to next page"
|
||||
>
|
||||
Next »
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
border: 1px solid var(--prj-link-text);
|
||||
padding: var(--prj-spacing-1) var(--prj-spacing-2);
|
||||
border-radius: var(--prj-border-radius);
|
||||
text-decoration: none;
|
||||
transition: background-color 400ms, color 400ms;
|
||||
|
||||
&.current {
|
||||
background-color: var(--prj-secondary);
|
||||
border: 1px solid var(--prj-secondary);
|
||||
color: var(--prj-secondary-text);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--prj-link-text);
|
||||
border: 1px solid var(--prj-link-text);
|
||||
color: var(--prj-accent-text);
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
63
_src/components/Spinner.astro
Normal file
63
_src/components/Spinner.astro
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
bgColor?: string;
|
||||
}
|
||||
|
||||
const {size = 200, color= "#cad3f5", bgColor} = Astro.props;
|
||||
---
|
||||
|
||||
<div class="spinner">
|
||||
<div class="container">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path class="animation animation-normal" d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4" stroke={color} stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path class="animation animation-reverse" d="M36 24C36 17.3726 30.6274 12 24 12C17.3726 12 12 17.3726 12 24C12 30.6274 17.3726 36 24 36V36" stroke={color} stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<style define:vars={{size: `${size}px`, bgColor: bgColor ?? "var(--prj-bg)"}}>
|
||||
.spinner {
|
||||
background: var(--bgColor);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 10px 0px -10px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
.animation {
|
||||
animation: rotate 1.5s linear infinite;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.animation-normal {
|
||||
animation-direction: normal;
|
||||
}
|
||||
|
||||
.animation-reverse {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
_src/components/Table/Filters/NumberFilter.tsx
Normal file
43
_src/components/Table/Filters/NumberFilter.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
onChange: (value: [string, number | null]) => void;
|
||||
keyData: string;
|
||||
}
|
||||
|
||||
export default function NumberInput({ keyData, onChange }: Props): JSX.Element {
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [operator, setOperator] = useState<string>('=');
|
||||
|
||||
useEffect(() => {
|
||||
onChange([operator, value === '' ? null : parseFloat(value)]);
|
||||
}, [value, operator]);
|
||||
|
||||
return (
|
||||
<div className="hstack">
|
||||
<select
|
||||
name={`number-select-${keyData}`}
|
||||
id={`number-select-${keyData}`}
|
||||
defaultValue={'='}
|
||||
onChange={({ target }) => {
|
||||
setOperator(target.value);
|
||||
}}
|
||||
>
|
||||
<option value="=">=</option>
|
||||
<option value=">">{'>'}</option>
|
||||
<option value="<">{'<'}</option>
|
||||
<option value=">=">{'>='}</option>
|
||||
<option value="<=">{'<='}</option>
|
||||
</select>
|
||||
<input
|
||||
name={`number-input-${keyData}`}
|
||||
id="foo"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
onChange={({ target }) => {
|
||||
setValue(target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
_src/components/Table/Filters/SelectFilter.tsx
Normal file
41
_src/components/Table/Filters/SelectFilter.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import type { DataItem, Value } from '../types';
|
||||
import SelectInput from '@components/Inputs/SelectInput';
|
||||
|
||||
interface Props {
|
||||
onChange: (value: Value) => void;
|
||||
data: DataItem[];
|
||||
keyData: string;
|
||||
isMultiple?: boolean;
|
||||
}
|
||||
|
||||
export default function SelectFilter({
|
||||
data,
|
||||
keyData,
|
||||
isMultiple = false,
|
||||
onChange,
|
||||
}: Props): JSX.Element {
|
||||
const options = useMemo(() => {
|
||||
let options = [];
|
||||
|
||||
if (isMultiple) {
|
||||
options = data.flatMap((item) => item[keyData]);
|
||||
} else {
|
||||
options = data.map((item) => item[keyData]);
|
||||
}
|
||||
|
||||
options = [...new Set(options)];
|
||||
|
||||
options = options.map((item) => ({ label: item, value: item }));
|
||||
|
||||
return options;
|
||||
}, [data, keyData]);
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
options={options}
|
||||
isMultiple={isMultiple}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
64
_src/components/Table/Filters/index.tsx
Normal file
64
_src/components/Table/Filters/index.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { HeaderType, type Filter, type Value } from '../types.ts';
|
||||
|
||||
export { default as SelectFilter } from './SelectFilter.tsx';
|
||||
export { default as NumberFilter } from './NumberFilter.tsx';
|
||||
|
||||
const filterString = (value: string, data: string): boolean => {
|
||||
return data.toLowerCase().search(value.toLowerCase()) !== -1;
|
||||
};
|
||||
const filterNumber = (value: Value, data: any): boolean => {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(
|
||||
'Value should be an array in the form of [operator: string, value: number]',
|
||||
);
|
||||
}
|
||||
|
||||
const [operator, numberValue] = value;
|
||||
|
||||
if (numberValue === null) return true;
|
||||
|
||||
switch (operator) {
|
||||
case '=':
|
||||
return data === numberValue;
|
||||
case '<':
|
||||
return data < numberValue;
|
||||
case '>':
|
||||
return data > numberValue;
|
||||
case '<=':
|
||||
return data <= numberValue;
|
||||
case '>=':
|
||||
return data >= numberValue;
|
||||
|
||||
default:
|
||||
return data === numberValue;
|
||||
}
|
||||
};
|
||||
const filterSelect = (value: Value, data: any): boolean => {
|
||||
return data === value;
|
||||
};
|
||||
const filterMultiple = (value: Value, data: any): boolean => {
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
return data.includes(value);
|
||||
}
|
||||
|
||||
return value.every((filter: string | number) => data.includes(filter));
|
||||
};
|
||||
|
||||
export const resolveFilterByType = (filter: Filter, data: any): boolean => {
|
||||
switch (filter.type) {
|
||||
case HeaderType.String:
|
||||
return filterString(filter.value, data);
|
||||
case HeaderType.Number:
|
||||
return filterNumber(filter.value, data);
|
||||
case HeaderType.Select:
|
||||
return filterSelect(filter.value, data);
|
||||
case HeaderType.Multiple:
|
||||
return filterMultiple(filter.value, data);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
6
_src/components/Table/Table.module.css
Normal file
6
_src/components/Table/Table.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.table th,
|
||||
.table td {
|
||||
padding: 0.25rem 1rem;
|
||||
border: 1px solid white;
|
||||
text-align: center;
|
||||
}
|
||||
197
_src/components/Table/Table.tsx
Normal file
197
_src/components/Table/Table.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import usePagination, { Offset } from 'src/hooks/usePagination';
|
||||
import { SelectFilter, NumberFilter, resolveFilterByType } from './Filters';
|
||||
import type { DataItem, Header, Value, Filter } from './types';
|
||||
import { HeaderType } from './types';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
interface Props {
|
||||
data: DataItem[];
|
||||
headers: Header[];
|
||||
}
|
||||
|
||||
export default function Table({ data, headers }: Props): JSX.Element {
|
||||
const [filters, setFilters] = useState<Record<string, Filter>>({});
|
||||
const filtersId = useRef(crypto.randomUUID());
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
return Object.entries(filters).every(([key, filter]) =>
|
||||
resolveFilterByType(filter, item[key]),
|
||||
);
|
||||
});
|
||||
}, [data, filters]);
|
||||
|
||||
const { items, changeOffset, getPaginationRange } = usePagination<DataItem>({
|
||||
items: filteredItems,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const handleUpdateFilters = (
|
||||
name: string,
|
||||
type: HeaderType,
|
||||
value: Value,
|
||||
): void => {
|
||||
setFilters((prev) => {
|
||||
if (value === null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete prev[name];
|
||||
return { ...prev };
|
||||
}
|
||||
|
||||
return { ...prev, [name]: { value, type } };
|
||||
});
|
||||
};
|
||||
|
||||
function formatCell(data: DataItem, header: Header): JSX.Element {
|
||||
// This formatting is only used because the source is trusted (private markdown files manage only by me)
|
||||
// and because Astro don't allow me to pass JSX from an Astro file to a TSX file,
|
||||
// so I have to pass the formatted row as a string.
|
||||
// DON'T use this method on a public API
|
||||
if (header.hasCustomCell && header.formatter) {
|
||||
return (
|
||||
<div dangerouslySetInnerHTML={{ __html: header.formatter(data) }} />
|
||||
);
|
||||
}
|
||||
|
||||
if (header.type === HeaderType.Multiple) {
|
||||
return (
|
||||
<ul className="text-start">
|
||||
{data[header.key].map((item: JSX.Element, idx: number) => (
|
||||
<li key={idx}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return data[header.key];
|
||||
}
|
||||
|
||||
function formatFilter(header: Header): JSX.Element {
|
||||
const baseProps = {
|
||||
key: header.key + filtersId.current,
|
||||
keyData: header.key,
|
||||
value: filters[header.key]?.value,
|
||||
onChange: (value: Value) => {
|
||||
handleUpdateFilters(header.key, header.type, value);
|
||||
},
|
||||
};
|
||||
|
||||
switch (header.type) {
|
||||
case HeaderType.String:
|
||||
return (
|
||||
<input
|
||||
onChange={(e) => {
|
||||
baseProps.onChange(e.target.value);
|
||||
}}
|
||||
key={baseProps.key}
|
||||
/>
|
||||
);
|
||||
case HeaderType.Number:
|
||||
return (
|
||||
<NumberFilter
|
||||
{...baseProps}
|
||||
onChange={(value: [string, number | null]) => {
|
||||
handleUpdateFilters(header.key, header.type, value as Value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case HeaderType.Select:
|
||||
return <SelectFilter data={data} {...baseProps} />;
|
||||
case HeaderType.Multiple:
|
||||
return <SelectFilter {...baseProps} isMultiple data={data} />;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="hstack">
|
||||
<button
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
setFilters({});
|
||||
filtersId.current = crypto.randomUUID();
|
||||
}}
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="mt-1 overflow-scroll">
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((item, idx) => (
|
||||
<th key={idx}>
|
||||
<div className="vstack">
|
||||
{item.header}
|
||||
{formatFilter(item)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{items.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
{headers.map((header, hidx) => (
|
||||
<td key={hidx}>{formatCell(item, header)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
changeOffset(Offset.First);
|
||||
}}
|
||||
>
|
||||
First
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
changeOffset(Offset.Prev);
|
||||
}}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
|
||||
{getPaginationRange().map((item) => (
|
||||
<button
|
||||
className={item.current ? 'btn-primary' : ''}
|
||||
key={item.page}
|
||||
onClick={() => {
|
||||
changeOffset(Offset.To, item.page);
|
||||
}}
|
||||
>
|
||||
{item.page}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
changeOffset(Offset.Next);
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
changeOffset(Offset.Last);
|
||||
}}
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
_src/components/Table/index.ts
Normal file
1
_src/components/Table/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Table';
|
||||
24
_src/components/Table/types.ts
Normal file
24
_src/components/Table/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export type DataItem = Record<string, any>;
|
||||
|
||||
export type Value = string | string[] | number | number[] | null;
|
||||
|
||||
export enum HeaderType {
|
||||
Index,
|
||||
String,
|
||||
Number,
|
||||
Select,
|
||||
Multiple,
|
||||
}
|
||||
|
||||
export interface Header {
|
||||
key: string;
|
||||
header: string;
|
||||
type: HeaderType;
|
||||
hasCustomCell?: boolean;
|
||||
formatter?: (data: any) => string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
type: HeaderType;
|
||||
value: Value;
|
||||
}
|
||||
0
_src/components/Toc/Toc.module.css
Normal file
0
_src/components/Toc/Toc.module.css
Normal file
26
_src/components/Toc/Toc.tsx
Normal file
26
_src/components/Toc/Toc.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
// import styles from './Toc.module.css';
|
||||
|
||||
interface Props {
|
||||
headings: MarkdownHeading[];
|
||||
}
|
||||
|
||||
// TODO: Change this for the floating container with Intersection Observer
|
||||
// FIXME: Create real nesting and not the ilussion of nesting (aka nested ul and not padding multiplied by depth)
|
||||
export default function Toc({ headings }: Props): JSX.Element {
|
||||
return (
|
||||
<ul className="mb-3">
|
||||
{headings.map((item, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
style={{ paddingLeft: item.depth > 1 ? 20 * item.depth : 0 }}
|
||||
>
|
||||
<a href={`#${item.slug}`}>
|
||||
{item.depth} - {item.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
2
_src/env.d.ts
vendored
Normal file
2
_src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
124
_src/hooks/usePagination.tsx
Normal file
124
_src/hooks/usePagination.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { useReducer } from 'react';
|
||||
|
||||
interface Page {
|
||||
page: number;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
interface IUsePagination<T> {
|
||||
items: T[];
|
||||
changeOffset: (offset: Offset, newValue?: number) => void;
|
||||
/**
|
||||
* Returns an array with the aviables pages to directly navigate.
|
||||
* This pages are centered in the current page and offest to 5 items to each side.
|
||||
|
||||
* @returns {Page}
|
||||
*/
|
||||
getPaginationRange: () => Page[];
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
items: T[];
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
offset: number;
|
||||
total: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
type ActionType =
|
||||
| { type: 'update'; value: any; name: string }
|
||||
| { type: 'update'; value: any; name: string };
|
||||
|
||||
export enum Offset {
|
||||
Next,
|
||||
Prev,
|
||||
First,
|
||||
Last,
|
||||
To,
|
||||
}
|
||||
|
||||
export default function usePagination<T>({
|
||||
items,
|
||||
limit = 30,
|
||||
}: Props<T>): IUsePagination<T> {
|
||||
const reducer = (state: State, action: ActionType): State => {
|
||||
switch (action.type) {
|
||||
case 'update':
|
||||
return {
|
||||
...state,
|
||||
[action.name]: action.value,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
offset: 0,
|
||||
total: 0,
|
||||
limit,
|
||||
});
|
||||
|
||||
const changeOffset = (offset: Offset, newValue: number = 1): void => {
|
||||
let value = 0;
|
||||
switch (offset) {
|
||||
case Offset.Next:
|
||||
value = state.offset + state.limit;
|
||||
break;
|
||||
case Offset.Prev:
|
||||
value = state.offset - state.limit;
|
||||
break;
|
||||
case Offset.First:
|
||||
value = 0;
|
||||
break;
|
||||
case Offset.Last:
|
||||
value = items.length - state.limit;
|
||||
break;
|
||||
case Offset.To:
|
||||
value = (newValue - 1) * state.limit;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (value < 0 || value > items.length - state.limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'update',
|
||||
name: 'offset',
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
function getPaginationRange(): Page[] {
|
||||
// NOTE: this is made to work with uneven numbers,
|
||||
// So the current page is always aligned in the center.
|
||||
const paginationToSides = 5;
|
||||
|
||||
const currentPage = Math.ceil(state.offset / state.limit) + 1;
|
||||
const lastPage = Math.ceil(items.length / state.limit);
|
||||
const start = Math.max(currentPage - paginationToSides - 1, 0);
|
||||
const end = Math.min(currentPage + paginationToSides, lastPage);
|
||||
|
||||
return Array.from(
|
||||
{
|
||||
length: end - start,
|
||||
},
|
||||
(_, idx) => {
|
||||
const page = idx + 1 + start;
|
||||
return { page, current: page === currentPage };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
changeOffset,
|
||||
getPaginationRange,
|
||||
items: items.slice(state.offset, state.offset + state.limit),
|
||||
};
|
||||
}
|
||||
82
_src/layouts/Layout.astro
Normal file
82
_src/layouts/Layout.astro
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import i18next, { t } from 'i18next';
|
||||
import { HeadHrefLangs } from 'astro-i18next/components';
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
import '../assets/style/style.scss';
|
||||
import Navbar from '@components/Navbar.astro';
|
||||
import Spinner from '@components/Spinner.astro';
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang={i18next.language}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="Astro description" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<HeadHrefLangs />
|
||||
<title>{title}</title>
|
||||
<ViewTransitions />
|
||||
|
||||
<!-- Reset and normilize styles -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gardevoir" />
|
||||
</head>
|
||||
<body transition:animate="fade">
|
||||
<header class="position-sticky py-1 py-lg-3">
|
||||
<Navbar />
|
||||
</header>
|
||||
<main>
|
||||
<div id="layout-loading-spinner" class="d-none">
|
||||
<Spinner />
|
||||
</div>
|
||||
<slot />
|
||||
</main>
|
||||
<style lang="scss">
|
||||
body {
|
||||
header {
|
||||
background-color: var(--prj-bg);
|
||||
border-radius: 0 0 var(--prj-border-radius) var(--prj-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
/* Position spinner in the center of the screen instead of the center of it's parent */
|
||||
/* This is because the height of the main div can change */
|
||||
/* but we still want the background to hide the content */
|
||||
#layout-loading-spinner :global(.spinner) :global(svg) {
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline>
|
||||
document.addEventListener('astro:before-preparation', (ev) => {
|
||||
const originalLoader = ev.loader;
|
||||
ev.loader = async function () {
|
||||
const spinner = document.querySelector('#layout-loading-spinner');
|
||||
|
||||
// Only show the animation if page load is > than timeout seconds
|
||||
const timeoutId = setTimeout(
|
||||
() => spinner.classList.remove('d-none'),
|
||||
200,
|
||||
);
|
||||
|
||||
await originalLoader();
|
||||
|
||||
// cancel timeout if is not run yet
|
||||
clearTimeout(timeoutId);
|
||||
// spinner.classList.add('d-none');
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
_src/pages/404.astro
Normal file
10
_src/pages/404.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
|
||||
changeLanguage("en");
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<h1>not found</h1>
|
||||
</Layout>
|
||||
26
_src/pages/blog/[...slug].astro
Normal file
26
_src/pages/blog/[...slug].astro
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Toc from "@components/Toc/Toc";
|
||||
|
||||
changeLanguage("en");
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const entries = await getCollection("blog");
|
||||
return entries.map((entry) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: entry,
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
const entry = Astro.props;
|
||||
const { Content, headings } = await entry.render();
|
||||
---
|
||||
|
||||
<Layout title={entry.data.title}>
|
||||
<Toc headings={headings} />
|
||||
|
||||
<Content />
|
||||
</Layout>
|
||||
45
_src/pages/blog/index.astro
Normal file
45
_src/pages/blog/index.astro
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
import { changeLanguage } from 'i18next';
|
||||
import { getCollection } from 'astro:content';
|
||||
import Layout from '@layouts/Layout.astro';
|
||||
import Table from '@components/Table';
|
||||
import { HeaderType, type Header } from '@components/Table/types';
|
||||
|
||||
changeLanguage('en');
|
||||
|
||||
const rawEntries = await getCollection('blog', ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
const entries = rawEntries.map((item, idx) => ({
|
||||
...item.data,
|
||||
id: idx + 1,
|
||||
slug: item.slug,
|
||||
}));
|
||||
const headers: Header[] = [
|
||||
{
|
||||
key: 'id',
|
||||
header: 'index',
|
||||
type: HeaderType.Index,
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
header: 'Title',
|
||||
hasCustomCell: true,
|
||||
formatter: (data) => `<a href="blog/${data.slug}">${data.title}</a>`,
|
||||
type: HeaderType.String,
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
header: 'Tags',
|
||||
type: HeaderType.Multiple,
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title="List of blog entries">
|
||||
<h1>Blog's entries</h1>
|
||||
|
||||
<section>
|
||||
<Table client:load data={entries} headers={headers} />
|
||||
</section>
|
||||
</Layout>
|
||||
10
_src/pages/es/404.astro
Normal file
10
_src/pages/es/404.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
|
||||
changeLanguage("es");
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<h1>not found</h1>
|
||||
</Layout>
|
||||
10
_src/pages/es/blog.astro
Normal file
10
_src/pages/es/blog.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
|
||||
changeLanguage("es");
|
||||
---
|
||||
|
||||
<Layout title="Blog">
|
||||
<h1>Blog</h1>
|
||||
</Layout>
|
||||
26
_src/pages/es/blog/[...slug].astro
Normal file
26
_src/pages/es/blog/[...slug].astro
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Toc from "@components/Toc/Toc";
|
||||
|
||||
changeLanguage("es");
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const entries = await getCollection("blog");
|
||||
return entries.map((entry) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: entry,
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
const entry = Astro.props;
|
||||
const { Content, headings } = await entry.render();
|
||||
---
|
||||
|
||||
<Layout title={entry.data.title}>
|
||||
<Toc headings={headings} />
|
||||
|
||||
<Content />
|
||||
</Layout>
|
||||
44
_src/pages/es/blog/index.astro
Normal file
44
_src/pages/es/blog/index.astro
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Table from "@components/Table";
|
||||
import { HeaderType, type Header } from "@components/Table/types";
|
||||
|
||||
changeLanguage("es");
|
||||
|
||||
const rawEntries = await getCollection("blog", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
const entries = rawEntries.map((item, idx) => ({
|
||||
...item.data,
|
||||
id: idx + 1,
|
||||
slug: item.slug,
|
||||
}));
|
||||
const headers: Header[] = [
|
||||
{
|
||||
key: "id",
|
||||
header: "index",
|
||||
type: HeaderType.Index,
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
header: "Title",
|
||||
formatter: (data) => `<a href="blog/${data.slug}">${data.title}</a>`,
|
||||
type: HeaderType.String,
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
header: "Tags",
|
||||
type: HeaderType.Multiple,
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title="List of blog entries">
|
||||
<h1>Blog's entries</h1>
|
||||
|
||||
<section>
|
||||
<Table client:load data={entries} headers={headers} />
|
||||
</section>
|
||||
</Layout>
|
||||
253
_src/pages/es/index.astro
Normal file
253
_src/pages/es/index.astro
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
---
|
||||
import { getCollection, getEntry } from "astro:content";
|
||||
import { t, changeLanguage } from "i18next";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import Card from "../../components/Card.astro";
|
||||
import LocalizedMarkdown from "@components/LocalizedMarkdown.astro";
|
||||
import Button from "../../components/Button/Button.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import portrait from "../../assets/images/portrait.jpg";
|
||||
|
||||
changeLanguage("es");
|
||||
|
||||
const blog = await getCollection("blog", ({ data }) => import.meta.env.PROD ? data.draft !== true : true);
|
||||
const portafolio = [
|
||||
await getEntry("portafolio", "piloto-go"),
|
||||
await getEntry("portafolio", "destino-temuco"),
|
||||
await getEntry("portafolio", "sercotec"),
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title="aleidk">
|
||||
<section
|
||||
id="hero"
|
||||
class="bg-image flex-center"
|
||||
style="--bg-image: url(https://placehold.co/600x400)"
|
||||
>
|
||||
<div class="vstack gap-0 align-items-center">
|
||||
<Image
|
||||
id="portrait"
|
||||
src={portrait}
|
||||
alt="portrait of Alexander Navarro"
|
||||
loading="eager"
|
||||
width={200}
|
||||
/>
|
||||
<div>
|
||||
<h1 class="text-center my-0">Alexander Navarro</h1>
|
||||
<div class="text-center" class="">
|
||||
<LocalizedMarkdown path="brief.md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-center">{t('titles.featuredWork')}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 grid-lg-cols-3 gap-4">
|
||||
{
|
||||
portafolio.map(({ data, slug }) => (
|
||||
<div>
|
||||
<Card className="anim-hover-zoom h-100">
|
||||
<a class="clean" href={`/projects/${slug}`}>
|
||||
<Image
|
||||
src={data.thumbnail}
|
||||
alt="project img"
|
||||
class="border-radius respect-width"
|
||||
/>
|
||||
<h3 class="fs-4 text-center my-1">{data.title}</h3>
|
||||
<p class="text-justify">{data.brief}</p>
|
||||
</a>
|
||||
<div class="text-end" slot="footer">
|
||||
<a href={`/projects/${slug}`}>{t('misc.seeMore')}</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<Button className="px-4 py-2 fs-5" href="/projects">
|
||||
{t('home.moreProjects')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class:list={[{ 'd-none': import.meta.env.PROD }]}>
|
||||
<h2 class="mb-4">{t('titles.whatIveBeenUpTo')}</h2>
|
||||
|
||||
<div class="hstack flex-eq flex-wrap flex-md-nowrap">
|
||||
<div class="vstack justify-content-center">
|
||||
<h4 class="text-center text-capitalize">{t('blog')}</h4>
|
||||
<ol class="list-unstyle mt-0 fs-5">
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-center mb-2">
|
||||
<img class="" src="https://placehold.co/400x200" alt="project img" />
|
||||
</div>
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Esse
|
||||
consequatur iste molestiae blanditiis eligendi consectetur ullam.
|
||||
Excepturi quasi sed est animi laudantium necessitatibus, tempore
|
||||
delectus nulla aspernatur quod nesciunt fugiat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hstack-reverse flex-eq flex-wrap flex-md-nowrap">
|
||||
<div class="vstack justify-content-center">
|
||||
<h4 class="text-center text-capitalize">{t('games')}</h4>
|
||||
<ol class="list-unstyle mt-0 fs-5">
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-center mb-2">
|
||||
<img class="" src="https://placehold.co/400x200" alt="project img" />
|
||||
</div>
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Esse
|
||||
consequatur iste molestiae blanditiis eligendi consectetur ullam.
|
||||
Excepturi quasi sed est animi laudantium necessitatibus, tempore
|
||||
delectus nulla aspernatur quod nesciunt fugiat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="bg-image"
|
||||
style="--bg-image: url(https://placehold.co/600x400)"
|
||||
>
|
||||
<h2 class="text-center">{t('titles.whoAmI')}</h2>
|
||||
<p class="w-95 w-lg-70 mx-auto text-justify">
|
||||
<LocalizedMarkdown path="about-me.md" />
|
||||
</p>
|
||||
|
||||
<div class="text-center fs-4 mb-0">
|
||||
<a href="">
|
||||
<Button>{t('home.viewCurriculum')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contact</h2>
|
||||
|
||||
<div class="grid grid-cols-1 grid-lg-cols-2">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<img
|
||||
class="respect-width border-radius"
|
||||
src="https://placehold.co/500x300"
|
||||
alt="project img"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facilis
|
||||
reprehenderit, porro dolorem cumque suscipit accusantium officiis eius
|
||||
exercitationem harum itaque perferendis praesentium asperiores vitae
|
||||
pariatur ad culpa mollitia necessitatibus hic!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="vstack justify-content-center">
|
||||
<ul
|
||||
class="list-unstyle fs-5 ml-lg-5 mt-3 d-flex d-lg-block justify-content-around anim-idle-hover-group anim-group-lg-none"
|
||||
>
|
||||
{
|
||||
[
|
||||
{
|
||||
link: '#',
|
||||
img: 'https://placehold.co/60',
|
||||
alt: '',
|
||||
text: 'Github',
|
||||
},
|
||||
{
|
||||
link: '#',
|
||||
img: 'https://placehold.co/60',
|
||||
alt: '',
|
||||
text: 'Linkedin',
|
||||
},
|
||||
{
|
||||
link: '#',
|
||||
img: 'https://placehold.co/60',
|
||||
alt: '',
|
||||
text: 'Email',
|
||||
},
|
||||
].map((item) => (
|
||||
<li class="mb-3">
|
||||
<a
|
||||
href={item.link}
|
||||
class="hstack flex-column flex-lg-row gap-2"
|
||||
>
|
||||
<>
|
||||
<img src={item.img} alt={item.alt} />
|
||||
<span>{item.text}</span>
|
||||
</>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#portrait {
|
||||
border-radius: 50%;
|
||||
/* border: 5px solid var(--prj-accent); */
|
||||
}
|
||||
#hero {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
a.clean {
|
||||
color: var(--prj-text);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</Layout>out> out>
|
||||
81
_src/pages/es/projects/[...slug].astro
Normal file
81
_src/pages/es/projects/[...slug].astro
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Toc from "@components/Toc/Toc";
|
||||
import Card from "@components/Card.astro";
|
||||
import Button from "@components/Button/Button.astro";
|
||||
|
||||
changeLanguage("es");
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const entries = await getCollection("portafolio");
|
||||
return entries.map((entry) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: entry,
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
const entry = Astro.props;
|
||||
const { Content, headings } = await entry.render();
|
||||
---
|
||||
|
||||
<Layout title={entry.data.title}>
|
||||
<h1>{entry.data.title}</h1>
|
||||
<Card className="w-lg-50 mx-auto">
|
||||
<div class="project-specs grid">
|
||||
<div class="project-spec-property">Timeframe:</div>
|
||||
<div class="project-spec-value">{entry.data.timeframe}</div>
|
||||
|
||||
<div class="project-spec-property">Repo:</div>
|
||||
<div class="project-spec-value">{entry.data.links?.url ?? 'Private'}</div>
|
||||
|
||||
<div class="project-spec-property">Website:</div>
|
||||
<div class="project-spec-value">
|
||||
{entry.data.links?.repo ?? 'Private'}
|
||||
</div>
|
||||
|
||||
<div class="project-spec-property">Technologies:</div>
|
||||
<div class="project-spec-value">
|
||||
<ul>
|
||||
{entry.data.technologies.map((item) => <li>{item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Toc headings={headings} />
|
||||
|
||||
<Content />
|
||||
|
||||
<div
|
||||
class="position-fixed bottom-0"
|
||||
style={{ top: 'unset', left: 'unset', bottom: '5%', right: '5%' }}
|
||||
>
|
||||
<Button className="btn-back">Go back</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style is:global lang="scss">
|
||||
img {
|
||||
margin: 0 auto var(--prj-spacing-2) auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.project-specs.grid {
|
||||
--prj-columns: 0.2fr 1fr;
|
||||
|
||||
.project-spec-property {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
document.querySelector('.btn-back')?.addEventListener('click', () => {
|
||||
history.back();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
65
_src/pages/es/projects/[page].astro
Normal file
65
_src/pages/es/projects/[page].astro
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
|
||||
import { changeLanguage } from "i18next";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Card from "@components/Card.astro";
|
||||
import Pagination from "@components/Pagination.astro";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
changeLanguage("es");
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const rawEntries = await getCollection("portafolio", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
const entries = rawEntries.map((item, idx) => ({
|
||||
...item.data,
|
||||
id: idx + 1,
|
||||
slug: item.slug,
|
||||
}));
|
||||
return paginate(entries, { pageSize: 6 });
|
||||
}) satisfies GetStaticPaths;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title="List of blog entries">
|
||||
<h1 class="text-center">Projects</h1>
|
||||
|
||||
<section class="clean">
|
||||
<div class="grid grid-cols-1 grid-lg-cols-3 gap-4">
|
||||
{
|
||||
page.data.map((item) => (
|
||||
<div>
|
||||
<Card className="anim-hover-zoom h-100">
|
||||
<a class="clean" href={`/projects/${item.slug}`}>
|
||||
<Image
|
||||
src={item.thumbnail}
|
||||
alt="project img"
|
||||
class="border-radius respect-width"
|
||||
/>
|
||||
<h3 class="fs-4 text-center my-1">{item.title}</h3>
|
||||
<p class="text-justify">{item.brief}</p>
|
||||
</a>
|
||||
<div class="text-end" slot="footer">
|
||||
<a href={`/projects/${item.slug}`}>See more...</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Pagination page={page} urlPattern="/projects/{}" />
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
a.clean {
|
||||
color: var(--prj-text);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
270
_src/pages/index.astro
Normal file
270
_src/pages/index.astro
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
---
|
||||
import { getCollection, getEntry } from 'astro:content';
|
||||
import { t, changeLanguage } from 'i18next';
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Card from '../components/Card.astro';
|
||||
import LocalizedMarkdown from '@components/LocalizedMarkdown.astro';
|
||||
import Button from '../components/Button/Button.astro';
|
||||
import { Image, getImage } from 'astro:assets';
|
||||
import portrait from '../assets/images/portrait.jpg';
|
||||
import iconEmail from '../assets/icons/email.svg';
|
||||
import iconLinkedin from '../assets/icons/linkedin.svg';
|
||||
import iconGithub from '../assets/icons/github.svg';
|
||||
import Background1 from '@assets/images/section-bg-1.png';
|
||||
|
||||
changeLanguage('en');
|
||||
|
||||
const blog = await getCollection('blog', ({ data }) =>
|
||||
import.meta.env.PROD ? data.draft !== true : true,
|
||||
);
|
||||
const portafolio = [
|
||||
await getEntry('portafolio', 'piloto-go'),
|
||||
await getEntry('portafolio', 'destino-temuco'),
|
||||
await getEntry('portafolio', 'sercotec'),
|
||||
];
|
||||
|
||||
const optimizedBackground = await getImage({
|
||||
src: Background1,
|
||||
format: 'webp',
|
||||
});
|
||||
---
|
||||
|
||||
<Layout title="aleidk">
|
||||
<section
|
||||
id="hero"
|
||||
class="bg-image flex-center"
|
||||
style={`--bg-image: url(${optimizedBackground.src})`}
|
||||
>
|
||||
<div class="vstack gap-0 align-items-center">
|
||||
<Image
|
||||
id="portrait"
|
||||
src={portrait}
|
||||
alt="portrait of Alexander Navarro"
|
||||
loading="eager"
|
||||
width={200}
|
||||
/>
|
||||
<div class="text">
|
||||
<h1 class="text-center my-0">Alexander Navarro</h1>
|
||||
<div class="text-center" class="">
|
||||
<LocalizedMarkdown path="brief.md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-center">{t('titles.featuredWork')}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 grid-lg-cols-3 gap-4">
|
||||
{
|
||||
portafolio.map(({ data, slug }) => (
|
||||
<div>
|
||||
<Card className="anim-hover-zoom h-100">
|
||||
<a class="clean" href={`/projects/${slug}`}>
|
||||
<Image
|
||||
src={data.thumbnail}
|
||||
alt="project img"
|
||||
class="border-radius respect-width"
|
||||
/>
|
||||
<h3 class="fs-4 text-center my-1">{data.title}</h3>
|
||||
<p class="text-justify">{data.brief}</p>
|
||||
</a>
|
||||
<div class="text-end" slot="footer">
|
||||
<a href={`/projects/${slug}`}>{t('misc.seeMore')}</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<Button className="px-4 py-2 fs-5" href="/projects">
|
||||
{t('home.moreProjects')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class:list={[{ 'd-none': import.meta.env.PROD }]}>
|
||||
<h2 class="mb-4">{t('titles.whatIveBeenUpTo')}</h2>
|
||||
|
||||
<div class="hstack flex-eq flex-wrap flex-md-nowrap">
|
||||
<div class="vstack justify-content-center">
|
||||
<h4 class="text-center text-capitalize">{t('blog')}</h4>
|
||||
<ol class="list-unstyle mt-0 fs-5">
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-center mb-2">
|
||||
<img class="" src="https://placehold.co/400x200" alt="project img" />
|
||||
</div>
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Esse
|
||||
consequatur iste molestiae blanditiis eligendi consectetur ullam.
|
||||
Excepturi quasi sed est animi laudantium necessitatibus, tempore
|
||||
delectus nulla aspernatur quod nesciunt fugiat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hstack-reverse flex-eq flex-wrap flex-md-nowrap">
|
||||
<div class="vstack justify-content-center">
|
||||
<h4 class="text-center text-capitalize">{t('games')}</h4>
|
||||
<ol class="list-unstyle mt-0 fs-5">
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="#"
|
||||
>Qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat.</a
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-center mb-2">
|
||||
<img class="" src="https://placehold.co/400x200" alt="project img" />
|
||||
</div>
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Esse
|
||||
consequatur iste molestiae blanditiis eligendi consectetur ullam.
|
||||
Excepturi quasi sed est animi laudantium necessitatibus, tempore
|
||||
delectus nulla aspernatur quod nesciunt fugiat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="bg-image"
|
||||
style={`--bg-image: url(${optimizedBackground.src})`}
|
||||
>
|
||||
<h2 class="text-center">{t('titles.whoAmI')}</h2>
|
||||
<p class="w-95 w-lg-70 mx-auto text-justify text">
|
||||
<LocalizedMarkdown path="about-me.md" />
|
||||
</p>
|
||||
|
||||
<div class="text-center fs-4 mb-0">
|
||||
<a href="">
|
||||
<Button>{t('home.viewCurriculum')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{t('titles.contact')}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 grid-lg-cols-2">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<img
|
||||
class="respect-width border-radius"
|
||||
src="https://placehold.co/500x300"
|
||||
alt="project img"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facilis
|
||||
reprehenderit, porro dolorem cumque suscipit accusantium officiis eius
|
||||
exercitationem harum itaque perferendis praesentium asperiores vitae
|
||||
pariatur ad culpa mollitia necessitatibus hic!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="vstack justify-content-center">
|
||||
<ul
|
||||
class="list-unstyle fs-5 ml-lg-5 mt-3 d-flex d-lg-block justify-content-around anim-idle-hover-group anim-group-lg-none"
|
||||
>
|
||||
{
|
||||
[
|
||||
{
|
||||
link: 'https://github.com/aleee-idk',
|
||||
img: iconGithub,
|
||||
alt: '',
|
||||
text: 'Github',
|
||||
},
|
||||
{
|
||||
link: 'https://www.linkedin.com/in/alexander-navarro-parra-0516062a3',
|
||||
img: iconLinkedin,
|
||||
alt: '',
|
||||
text: 'Linkedin',
|
||||
},
|
||||
{
|
||||
link: 'mailto:ale.navarro.parra@gmail.com',
|
||||
img: iconEmail,
|
||||
alt: '',
|
||||
text: 'Email',
|
||||
},
|
||||
].map((item) => (
|
||||
<li class="mb-3">
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
class="hstack flex-column flex-lg-row gap-2"
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
src={item.img}
|
||||
alt={item.alt}
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
<span>{item.text}</span>
|
||||
</>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#portrait {
|
||||
border-radius: 50%;
|
||||
/* border: 5px solid var(--prj-accent); */
|
||||
}
|
||||
#hero {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
a.clean {
|
||||
color: var(--prj-text);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</Layout>out> out>
|
||||
81
_src/pages/projects/[...slug].astro
Normal file
81
_src/pages/projects/[...slug].astro
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
import { changeLanguage } from "i18next";
|
||||
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Toc from "@components/Toc/Toc";
|
||||
import Card from "@components/Card.astro";
|
||||
import Button from "@components/Button/Button.astro";
|
||||
|
||||
changeLanguage("en");
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const entries = await getCollection("portafolio");
|
||||
return entries.map((entry) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: entry,
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
const entry = Astro.props;
|
||||
const { Content, headings } = await entry.render();
|
||||
---
|
||||
|
||||
<Layout title={entry.data.title}>
|
||||
<h1>{entry.data.title}</h1>
|
||||
<Card className="w-lg-50 mx-auto">
|
||||
<div class="project-specs grid">
|
||||
<div class="project-spec-property">Timeframe:</div>
|
||||
<div class="project-spec-value">{entry.data.timeframe}</div>
|
||||
|
||||
<div class="project-spec-property">Repo:</div>
|
||||
<div class="project-spec-value">{entry.data.links?.url ?? 'Private'}</div>
|
||||
|
||||
<div class="project-spec-property">Website:</div>
|
||||
<div class="project-spec-value">
|
||||
{entry.data.links?.repo ?? 'Private'}
|
||||
</div>
|
||||
|
||||
<div class="project-spec-property">Technologies:</div>
|
||||
<div class="project-spec-value">
|
||||
<ul>
|
||||
{entry.data.technologies.map((item) => <li>{item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Toc headings={headings} />
|
||||
|
||||
<Content />
|
||||
|
||||
<div
|
||||
class="position-fixed bottom-0"
|
||||
style={{ top: 'unset', left: 'unset', bottom: '5%', right: '5%' }}
|
||||
>
|
||||
<Button className="btn-back">Go back</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style is:global lang="scss">
|
||||
img {
|
||||
margin: 0 auto var(--prj-spacing-2) auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.project-specs.grid {
|
||||
--prj-columns: 0.2fr 1fr;
|
||||
|
||||
.project-spec-property {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
document.querySelector('.btn-back')?.addEventListener('click', () => {
|
||||
history.back();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
65
_src/pages/projects/[page].astro
Normal file
65
_src/pages/projects/[page].astro
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
|
||||
import { changeLanguage } from "i18next";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Card from "@components/Card.astro";
|
||||
import Pagination from "@components/Pagination.astro";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
changeLanguage("en");
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const rawEntries = await getCollection("portafolio", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
const entries = rawEntries.map((item, idx) => ({
|
||||
...item.data,
|
||||
id: idx + 1,
|
||||
slug: item.slug,
|
||||
}));
|
||||
return paginate(entries, { pageSize: 6 });
|
||||
}) satisfies GetStaticPaths;
|
||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title="List of blog entries">
|
||||
<h1 class="text-center">Projects</h1>
|
||||
|
||||
<section class="clean">
|
||||
<div class="grid grid-cols-1 grid-lg-cols-3 gap-4">
|
||||
{
|
||||
page.data.map((item) => (
|
||||
<div>
|
||||
<Card className="anim-hover-zoom h-100">
|
||||
<a class="clean" href={`/projects/${item.slug}`}>
|
||||
<Image
|
||||
src={item.thumbnail}
|
||||
alt="project img"
|
||||
class="border-radius respect-width"
|
||||
/>
|
||||
<h3 class="fs-4 text-center my-1">{item.title}</h3>
|
||||
<p class="text-justify">{item.brief}</p>
|
||||
</a>
|
||||
<div class="text-end" slot="footer">
|
||||
<a href={`/projects/${item.slug}`}>See more...</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Pagination page={page} urlPattern="/projects/{}" />
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
a.clean {
|
||||
color: var(--prj-text);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue