diff --git a/astro.config.mjs b/astro.config.mjs
index 3c276c6..35628eb 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,7 +1,12 @@
import { defineConfig } from 'astro/config';
+import react from "@astrojs/react";
+
// https://astro.build/config
export default defineConfig({
site: "https://retrozinndev.github.io",
- base: "."
+ base: ".",
+ integrations: [
+ react()
+ ],
});
diff --git a/package.json b/package.json
index f95de07..bc7f8ec 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,15 @@
},
"dependencies": {
"@astrojs/check": "^0.9.4",
+ "@astrojs/react": "^4.2.7",
+ "@octokit/rest": "github:octokit/rest.js",
+ "@types/react": "^19.1.4",
+ "@types/react-dom": "^19.1.5",
"astro": "^5.1.7",
+ "nanostores": "^1.0.1",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-icons": "^5.5.0",
"typescript": "^5.4.5"
},
"devDependencies": {
diff --git a/src/components/LanguageSelector.tsx b/src/components/LanguageSelector.tsx
new file mode 100644
index 0000000..bed0568
--- /dev/null
+++ b/src/components/LanguageSelector.tsx
@@ -0,0 +1,25 @@
+import { i18n, languages } from "../i18n/ui";
+
+type Props = {
+ visible?: boolean;
+ defaultLanguage?: string;
+};
+
+export default function(props: Props) {
+ return
+}
diff --git a/src/components/LinkButton.astro b/src/components/LinkButton.astro
index f6355c2..531b5ce 100644
--- a/src/components/LinkButton.astro
+++ b/src/components/LinkButton.astro
@@ -1,15 +1,26 @@
---
+import { FaArrowUpRightFromSquare } from 'react-icons/fa6';
+
interface Props {
href: string;
external?: boolean;
+ showIcon?: boolean;
+ title?: string;
+ spacing?: boolean;
}
-const { href, external } = Astro.props;
+const { href, external, title, showIcon = true, spacing = true } = Astro.props;
---
-
-
+
+
+
+
diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro
deleted file mode 100644
index 9690188..0000000
--- a/src/components/Navigation.astro
+++ /dev/null
@@ -1,204 +0,0 @@
----
-import { Image } from "astro:assets";
-import { useTranslations } from "../i18n/utils.ts";
-import { i18n, languages } from "../i18n/ui.ts";
-
-const pages = [ "stack", "projects" ];
-const lang = Astro.params.lang || "en";
-const tr = useTranslations(lang as never);
----
-
-
-
-
-
-
-
-
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
new file mode 100644
index 0000000..6f09986
--- /dev/null
+++ b/src/components/Navigation.tsx
@@ -0,0 +1,109 @@
+import "../styles/components/Navigation.scss";
+
+import { useEffect, useState, type ReactElement } from "react";
+
+import { useTranslations } from "../i18n/utils";
+import { defaultLang } from "../i18n/ui";
+import ToggleThemeButton from "./ToggleThemeButton";
+import LanguageSelector from "./LanguageSelector";
+import { AstroJSX } from "astro/jsx-runtime";
+
+interface Props {
+ lang?: string;
+ showLanguageSelector?: boolean;
+ showProfilePic?: boolean;
+};
+
+type Page = {
+ name: string;
+ href: string|URL;
+ hasI18n?: boolean;
+};
+
+const pages: Array = [
+ {
+ name: "home",
+ href: "/",
+ hasI18n: true
+ },
+ {
+ name: "about",
+ href: "/#about",
+ hasI18n: true
+ },
+ {
+ name: "stack",
+ href: "/stack",
+ hasI18n: true
+ },
+ {
+ name: "projects",
+ href: "/projects",
+ hasI18n: true
+ }
+];
+
+export default function(props: Props): ReactElement {
+ const showProfilePic = props.showProfilePic ?? true;
+ const lang = props.lang ?? defaultLang;
+ const tr = useTranslations(lang);
+
+ const [scrolled, setScrolledState] = useState(false);
+
+ useEffect(() => {
+ function handleScroll() {
+ if(window.scrollY >= 24) {
+ setScrolledState(true);
+ return;
+ }
+
+ setScrolledState(false);
+ }
+
+ window.addEventListener("scroll", handleScroll);
+ });
+
+ return
+
+
;
+}
diff --git a/src/components/ProjectCard.astro b/src/components/ProjectCard.astro
index 8abde29..489c034 100644
--- a/src/components/ProjectCard.astro
+++ b/src/components/ProjectCard.astro
@@ -16,7 +16,8 @@ const { icon, developer, title, description, href } = Astro.props;
+ } alt="dev profile picture" width={32} height={32} loading={"lazy"}
+ />
{ developer }
diff --git a/src/components/ProjectsComponent.astro b/src/components/ProjectsComponent.astro
index ac05296..39f98f0 100644
--- a/src/components/ProjectsComponent.astro
+++ b/src/components/ProjectsComponent.astro
@@ -1,48 +1,11 @@
---
-import { useTranslations } from "../i18n/utils";
import ProjectCard, { type Project } from "./ProjectCard.astro";
-const tr = useTranslations(Astro.params.lang as never || "en");
-
-const projects: Array = [
- {
- title: "colorshell",
- developer: "retrozinndev",
- description: "Super cool desktop shell for Hyprland!",
- href: "https://github.com/retrozinndev/colorshell"
- },
- {
- title: "retrozinndev.github.io",
- developer: "retrozinndev",
- description: "My personal website",
- href: "https://github.com/retrozinndev/retrozinndev.github.io"
- },
- {
- title: "parceirtoti.github.io",
- developer: "parceiroti",
- description: "ParceiroTI's website redesign",
- href: "https://github.com/parceiroti/parceiroti.github.io"
- },
- {
- title: "UpDateN'Time",
- developer: "retrozinndev",
- description: "A Tool that synchronizes your date and time with the internet",
- href: "https://github.com/retrozinndev/UpDateNTime"
- },
- {
- title: "The Traveler",
- developer: "notestudios",
- description: "A two-dimensional shooter game",
- href: "https://github.com/notestudios/TheTraveler"
- },
- {
- title: "Space Shooter",
- developer: "notestudios",
- description: "Shoot meteors in Space!",
- href: "https://github.com/notestudios/SpaceShooter"
- }
-];
+interface Props {
+ projects: Array;
+}
+const { projects } = Astro.props;
---
{
diff --git a/src/components/SocialLinks.astro b/src/components/SocialLinks.astro
index 09890a7..6b0631a 100644
--- a/src/components/SocialLinks.astro
+++ b/src/components/SocialLinks.astro
@@ -1,16 +1,70 @@
---
-// Discord User Widget by vendicated (https://vendicated.dev/)
+import type { IconType } from "react-icons";
+import { FaGithub, FaGitlab, FaXTwitter, FaReddit, FaDiscord } from "react-icons/fa6";
+import { FiMail } from "react-icons/fi";
import LinkButton from "./LinkButton.astro";
+
+type SocialLink = {
+ name: string;
+ href: string;
+ icon: IconType
+};
+
+const socials: Array
= [
+ {
+ name: "GitHub",
+ href: "https://github.com/retrozinndev",
+ icon: FaGithub
+ },
+ {
+ name: "GitLab",
+ href: "https://gitlab.com/retrozinndev",
+ icon: FaGitlab
+ },
+ {
+ name: "Discord",
+ href: "https://discord.com/users/568589231954591749",
+ icon: FaDiscord
+ },
+ {
+ name: "X / Twitter",
+ href: "https://x.com/retrozinndev",
+ icon: FaXTwitter
+ },
+ {
+ name: "Reddit",
+ href: "https://reddit.com/user/Much_Clue7037",
+ icon: FaReddit
+ },
+ {
+ name: "Mail",
+ href: "mailto:joaovodias@gmail.com",
+ icon: FiMail
+ }
+];
---
+
+ {
+ socials.map(social =>
+ -
+
+
+
+
+ )
+}
+
-
-
- GitHub |
- retrozinndev |
-
-
- GitLab |
- retrozinndev |
-
-
- CodeBerg |
- retrozinndev |
-
-
- X / Twitter |
- @retrozinndev |
-
-
- Matrix |
- @retrozinndev:matrix.org |
-
-
- Reddit |
- Much_Clue7037 |
-
-
- Mail |
- joaovodias@gmail.com |
-
-
-
-
diff --git a/src/components/ToggleThemeButton.tsx b/src/components/ToggleThemeButton.tsx
new file mode 100644
index 0000000..99056fd
--- /dev/null
+++ b/src/components/ToggleThemeButton.tsx
@@ -0,0 +1,32 @@
+import { useState, type ReactElement } from "react"
+import { getStoredTheme, getTheme, toggleTheme } from "../scripts/theme";
+import { RiMoonLine, RiSunLine } from "react-icons/ri";
+import { IconContext } from "react-icons";
+
+export default function(props: { initialTheme?: "dark" | "light" }): ReactElement {
+ const [stateTheme, setStateTheme] = useState(props.initialTheme ?? getStoredTheme()) as [
+ ("dark" | "light"), React.Dispatch>
+ ];
+
+ return ( {
+ toggleTheme();
+ setStateTheme(getTheme() as ("dark" | "light"));
+ }} onLoad={() =>
+ getStoredTheme() !== stateTheme &&
+ setStateTheme(getStoredTheme() as ("dark" | "light"))
+ } style={{
+ cursor: "pointer"
+ }}>
+ {
+ stateTheme === "dark" ?
+
+ :
+ }
+
);
+}
diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts
index 1c0a801..0890778 100644
--- a/src/i18n/utils.ts
+++ b/src/i18n/utils.ts
@@ -2,14 +2,12 @@
import type { i18nStruct } from "./struct.ts";
import { languages, i18n, defaultLang } from "./ui.ts";
-export function getLanguageFromURL(url: (URL|string)): (string) {
- const splittedURL: string[] = (typeof url === "string") ?
- url.split('/')
- : url.pathname.split("/");
-
- return splittedURL[1].length !== 2 ?
- defaultLang
- : splittedURL[1];
+export function getLanguageFromURL(url: URL): (string|undefined) {
+ const splitPath = url.pathname.split('/', 2);
+
+ return (splitPath[1] ?? false) && splitPath[1].length <= 5 ?
+ splitPath[1]
+ : undefined;
}
export function useTranslations(lang: string): Function {
diff --git a/src/layouts/PageLayout.astro b/src/layouts/PageLayout.astro
index e0d1c12..977149c 100644
--- a/src/layouts/PageLayout.astro
+++ b/src/layouts/PageLayout.astro
@@ -1,43 +1,54 @@
---
-import Footer from "../components/Footer.astro";
-import Navigation from "../components/Navigation.astro";
import "../styles/global.scss";
+import Footer from "../components/Footer.astro";
+import Navigation from "../components/Navigation.tsx";
+
+import { defaultLang } from "../i18n/ui";
+
interface Props {
title: string;
icon?: string;
+ lang?: string;
+ navSpacing?: string;
+ sideSpacing?: string;
+ flex?: boolean;
+ flexDirection?: "row" | "column" | "row-reverse" | "column-reverse";
+ pfpOnNavigation?: boolean;
}
-const { title, icon } = Astro.props;
+const {
+ title, icon, navSpacing = "3em",
+ sideSpacing = "2em",
+ flex, pfpOnNavigation = true,
+ flexDirection = "row", lang = (Astro.params.lang ?? defaultLang)
+} = Astro.props;
---
-
+
{ title }
+ } />
-
+
-
-
+
+
+
+
-
-
diff --git a/src/pages/404.astro b/src/pages/404.astro
index e411685..f1842c1 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -1,21 +1,18 @@
---
import { Image } from "astro:assets";
-import { getLanguages, getLanguageFromURL, useTranslations } from "../i18n/utils";
+import { getLanguageFromURL, useTranslations } from "../i18n/utils";
import PageLayout from "../layouts/PageLayout.astro";
+import { defaultLang, languages } from "../i18n/ui";
-Astro.params.lang = "en";
+const urlLang = getLanguageFromURL(Astro.url);
+const lang = urlLang && languages.includes(urlLang, 0) ?
+ urlLang
+: defaultLang;
-getLanguages().forEach(intl => {
- if(getLanguageFromURL(Astro.url) !== intl)
- return;
-
- Astro.params.lang = intl;
-});
-
-const tr = useTranslations(Astro.params.lang);
+const tr = useTranslations(lang);
---
-
+
diff --git a/src/pages/[lang]/index.astro b/src/pages/[lang]/index.astro
index 45d14fc..0f18fca 100644
--- a/src/pages/[lang]/index.astro
+++ b/src/pages/[lang]/index.astro
@@ -1,53 +1,142 @@
---
+import { Image } from "astro:assets";
import { getStaticPaths } from "../../i18n/route.ts";
import { useTranslations } from "../../i18n/utils.ts";
-import SocialLinks from '../../components/SocialLinks.astro';
import PageLayout from '../../layouts/PageLayout.astro';
+import SocialLinks from "../../components/SocialLinks.astro";
import LinkButton from "../../components/LinkButton.astro";
-import "../../styles/_colors.scss";
+import ProjectsComponent from "../../components/ProjectsComponent.astro";
+import { projects } from "./projects.astro";
export { getStaticPaths };
const tr = useTranslations(Astro.params.lang as never);
---
-
+
+
+
+
+
+
+
-
-
-
{ tr("about.title") } João!
-
-
-
{ tr("about.description") }
+
+
{tr("about.title")} João!
+
{
+ tr("about.description")
+ }
+
+
{tr("about.socials_title")}
+
+
+
+
+
-
-
- { tr("about.projects") }
-
-
-
-
{ tr("about.socials_title") }
-
+
+
+
{ tr("projects.title") }
+
+ { tr("projects.description") }
+ GitHub
+
+
-
diff --git a/src/pages/[lang]/projects.astro b/src/pages/[lang]/projects.astro
index c14723b..91fa236 100644
--- a/src/pages/[lang]/projects.astro
+++ b/src/pages/[lang]/projects.astro
@@ -3,10 +3,50 @@ import ProjectsComponent from "../../components/ProjectsComponent.astro";
import { getStaticPaths } from "../../i18n/route.ts";
import { useTranslations } from "../../i18n/utils";
import PageLayout from "../../layouts/PageLayout.astro";
+import type { Project } from "../../components/ProjectCard.astro";
export { getStaticPaths };
const tr = useTranslations(Astro.params.lang as never);
+
+export const projects: Array
= [
+ {
+ title: "colorshell",
+ developer: "retrozinndev",
+ description: "Super cool desktop shell for Hyprland!",
+ href: "https://github.com/retrozinndev/colorshell"
+ },
+ {
+ title: "retrozinndev.github.io",
+ developer: "retrozinndev",
+ description: "My personal website",
+ href: "https://github.com/retrozinndev/retrozinndev.github.io"
+ },
+ {
+ title: "parceirtoti.github.io",
+ developer: "parceiroti",
+ description: "ParceiroTI's website redesign",
+ href: "https://github.com/parceiroti/parceiroti.github.io"
+ },
+ {
+ title: "UpDateN'Time",
+ developer: "retrozinndev",
+ description: "A Tool that synchronizes your date and time with the internet",
+ href: "https://github.com/retrozinndev/UpDateNTime"
+ },
+ {
+ title: "The Traveler",
+ developer: "notestudios",
+ description: "A two-dimensional shooter game",
+ href: "https://github.com/notestudios/TheTraveler"
+ },
+ {
+ title: "Space Shooter",
+ developer: "notestudios",
+ description: "Shoot meteors in Space!",
+ href: "https://github.com/notestudios/SpaceShooter"
+ }
+];
---
@@ -14,10 +54,10 @@ const tr = useTranslations(Astro.params.lang as never);
{ tr("projects.title") }
{ tr("projects.description") }
- GitHub.
+ GitHub.
+ Others
{
+ techs["others"].map((tech) =>
+
+
+ { tech.name }
+ )
+ }
diff --git a/src/pages/index.astro b/src/pages/index.astro
index a4bff50..0489060 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,7 +1,7 @@
-
+
-
diff --git a/src/scripts/repositories.ts b/src/scripts/repositories.ts
deleted file mode 100644
index 9623a8b..0000000
--- a/src/scripts/repositories.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/* This script gets Github repositories data and contains special types for it */
-// This script is not ready to use
-
-export type User = {
- display_name: string,
- name: string,
- url: string
-};
-
-export type Organization = {
- display_name: string,
- name: string,
- url: string,
- founder: User,
- members: Array,
- members_count: number
-};
-
-export type Repository = {
- name: string,
- description: string,
- developer: (User|Organization),
- committers: Array
-};
-
-// Need to finish
-export async function fetchRepo(repo: Repository) {
- const response: Response = await fetch("", {
- method: "POST",
- body: JSON.stringify({
-
- }),
- });
-}
diff --git a/src/scripts/stores.ts b/src/scripts/stores.ts
new file mode 100644
index 0000000..a2192b2
--- /dev/null
+++ b/src/scripts/stores.ts
@@ -0,0 +1,26 @@
+import { atom, type PreinitializedWritableAtom } from "nanostores";
+
+export type AtomStoreType = PreinitializedWritableAtom & object & {
+ key: string;
+};
+
+const stores: Record> = {} as const;
+export function addStore(key: string, initialValue: StoreType): AtomStoreType {
+ if(Object.hasOwn(stores, key)) {
+ console.warn(`Stores: there's already a key named \`${key}\``);
+ return stores[key as keyof typeof stores];
+ }
+
+ stores[key] = atom(initialValue);
+ return stores[key];
+}
+
+export function removeStore(key: string): boolean {
+ if(Object.hasOwn(stores, key)) {
+ stores[key as keyof typeof stores].off();
+ delete stores[key as keyof typeof stores];
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/scripts/theme.ts b/src/scripts/theme.ts
index 23c854b..95e42d7 100644
--- a/src/scripts/theme.ts
+++ b/src/scripts/theme.ts
@@ -11,7 +11,10 @@ export function getThemes(): Array {
}
export function getStoredTheme(): (string|undefined) {
- return localStorage.getItem("theme")?.toString();
+ if(typeof localStorage !== "undefined")
+ return localStorage.getItem("theme")?.toString() ?? getThemes()[0];
+
+ return getThemes()[0];
}
export function toggleTheme(): void {
diff --git a/src/styles/_colors.scss b/src/styles/_colors.scss
index 93ae46a..96b2aa1 100644
--- a/src/styles/_colors.scss
+++ b/src/styles/_colors.scss
@@ -10,6 +10,6 @@ $link-color: #2f82c2;
$emphasis-color: #2c73a5;
// General
-$accent-color: #e3cf62;
$accent-color-dark: #a89b54;
+$accent-color: #e3cf62;
diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss
index 49fd042..56dc8ff 100644
--- a/src/styles/_fonts.scss
+++ b/src/styles/_fonts.scss
@@ -1,7 +1 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
-@import url("https://www.nerdfonts.com/assets/css/webfont.css");
-
-@font-face {
- font-family: "Nerd Font Symbols";
- src: url("https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/NerdFontsSymbolsOnly/SymbolsNerdFontMono-Regular.ttf");
-}
diff --git a/src/styles/components/Navigation.scss b/src/styles/components/Navigation.scss
new file mode 100644
index 0000000..fdae39c
--- /dev/null
+++ b/src/styles/components/Navigation.scss
@@ -0,0 +1,124 @@
+@use "sass:color";
+@use "../colors";
+
+$padding-around: 2px;
+
+@function calcStickyTop($padd) {
+ @if($padd < 2px) {
+ @return calc($padd * 2 + 2);
+ }
+
+ @return calc($padd * 2);
+}
+
+.navigation-bar {
+ position: sticky;
+ display: block;
+ padding: {
+ left: $padding-around;
+ right: $padding-around;
+ top: $padding-around;
+ };
+
+ max-width: 100%;
+ top: calcStickyTop($padding-around);
+ z-index: 10;
+}
+
+#navbar {
+ display: flex;
+ border-radius: 22px;
+ backdrop-filter: blur(40px);
+ font-size: medium;
+ padding: 6px 20px;
+ min-height: 60px;
+ transition: 120ms ease-in-out;
+
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+
+ & .items {
+ display: flex;
+ width: 100%;
+ flex: 1 1 0;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ & > .item {
+ margin: 2px;
+ padding: 6px 8px;
+ border-radius: 12px;
+ transition: 200ms ease-in-out;
+
+ &, & * {
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ & *:is(a) {
+ all: unset;
+ -webkit-tap-highlight-color: unset;
+ color: inherit;
+ font-weight: 600;
+ font-size: 1.05em;
+ }
+
+ &:has(> a):hover,
+ &:has(> a).current {
+ background: colors.$accent-color-dark;
+ cursor: pointer;
+ }
+ }
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+}
+
+.navigation {
+ background: color.scale($color: colors.$bg-navbar-color-dark, $alpha: -20%);
+}
+
+body.light .navigation {
+ background-color: color.scale($color: colors.$bg-navbar-color-light, $alpha: -20%);
+}
+
+body.dark .navigation.scrolled {
+ box-shadow: 0 0 25px 2px rgba($color: black, $alpha: .6);
+}
+
+body.light .navigation.scrolled {
+ box-shadow: 0 0 5px 2px rgba($color: darkgray, $alpha: .6);
+}
+
+.left-side {
+ & img {
+ border-radius: 8px;
+ }
+}
+
+.language-selector > * {
+ outline: none;
+ background: none;
+ box-shadow: none;
+ border: 1px solid gray;
+ padding-inline: 10px;
+ padding-block: 4px;
+ border-radius: 8px;
+}
+
+@media screen and (max-width: 700px) {
+ #navbar {
+ padding-block: 14px;
+ justify-content: center;
+
+ & .items {
+ flex: unset;
+ }
+ }
+}
diff --git a/src/styles/global.scss b/src/styles/global.scss
index b16bff5..52b84e5 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -1,5 +1,6 @@
-@use "./colors";
+@use "sass:color";
@use "./fonts";
+@use "./colors";
html {
font-family: Noto Sans, Segoe UI, sans-serif;
@@ -53,10 +54,6 @@ h3 {
font-size: 18px;
}
-.nf {
- font-family: "Nerd Font Symbols";
-}
-
.center {
width: 100%;
text-align: center;
@@ -66,16 +63,14 @@ h3 {
transform: translate(0, -50%);
}
-.highlight {
- position: flex;
- font-style: bolder;
+span.highlight {
font-weight: bolder;
- color: colors.$accent-color;
+ color: colors.$accent-color-dark;
transition: ease-in-out 500ms;
}
-.highlight:hover {
- color: colors.$accent-color;
+body.light span.highlight {
+ color: color.scale($color: colors.$accent-color-dark, $lightness: 12%);
}
iframe {
diff --git a/tsconfig.json b/tsconfig.json
index 77da9dd..b7243b9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,3 +1,7 @@
{
- "extends": "astro/tsconfigs/strict"
-}
\ No newline at end of file
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react"
+ }
+}
{ tr("about.socials_title") }
-{ tr("projects.title") }
++ { tr("projects.description") } +GitHub
+
+