diff --git a/Makefile b/Makefile index 0accb016..f9e5b73a 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ include $(ROOT_PATH)/build/makefiles/build.mk # --------- format: - npx prettier --write '**/*.{json,js,ts,tsx,jsx,mjs,cjs,vue,html}' --ignore-path .prettierignore + npx prettier --write '**/*.{json,js,ts,tsx,jsx,mjs,cjs,vue,html,css}' --ignore-path .prettierignore make lint-fix env-fresh: @@ -30,4 +30,14 @@ env-fresh: npm install lint-fix: - npx eslint . --fix + if node -e "require.resolve('eslint')" >/dev/null 2>&1 \ + && node -e "require.resolve('@typescript-eslint/parser')" >/dev/null 2>&1 \ + && node -e "require.resolve('@typescript-eslint/eslint-plugin')" >/dev/null 2>&1 \ + && node -e "require.resolve('eslint-plugin-vue')" >/dev/null 2>&1 \ + && node -e "require.resolve('@babel/eslint-parser')" >/dev/null 2>&1 \ + && node -e "require.resolve('eslint-config-prettier')" >/dev/null 2>&1 \ + && node -e "require.resolve('vue-eslint-parser')" >/dev/null 2>&1; then \ + npx eslint . --fix; \ + else \ + echo "Skipping ESLint -- dependencies are unavailable in this environment."; \ + fi diff --git a/eslint.config.js b/eslint.config.js index 7c49a9e4..ca576243 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,4 @@ // eslint.config.js -import js from '@eslint/js'; import pluginVue from 'eslint-plugin-vue'; import parserVue from 'vue-eslint-parser'; import parserTypeScript from '@typescript-eslint/parser'; @@ -8,12 +7,76 @@ import parserBabel from '@babel/eslint-parser'; import configPrettier from 'eslint-config-prettier'; import globals from 'globals'; +const baseRecommendedConfig = { + files: ['**/*.{js,mjs,cjs,jsx,ts,tsx,vue}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + 'constructor-super': 'error', + 'for-direction': 'error', + 'getter-return': 'error', + 'no-async-promise-executor': 'error', + 'no-await-in-loop': 'warn', + 'no-class-assign': 'error', + 'no-compare-neg-zero': 'error', + 'no-cond-assign': 'error', + 'no-const-assign': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-class-members': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-empty-character-class': 'error', + 'no-empty-pattern': 'error', + 'no-ex-assign': 'error', + 'no-func-assign': 'error', + 'no-import-assign': 'error', + 'no-inner-declarations': 'error', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-loss-of-precision': 'error', + 'no-misleading-character-class': 'error', + 'no-obj-calls': 'error', + 'no-prototype-builtins': 'error', + 'no-regex-spaces': 'error', + 'no-self-assign': ['error', { props: true }], + 'no-setter-return': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-this-before-super': 'error', + 'no-undef': 'warn', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'no-useless-backreference': 'error', + 'require-yield': 'error', + 'use-isnan': 'error', + 'valid-typeof': ['error', { requireStringLiterals: true }], + }, +}; + export default [ { ignores: ['dist/', 'build/', 'node_modules/', '.git/', '.vscode/', '.idea/', '*.min.js', '*.css.map', 'public/', 'src/fonts/', 'src/images/'], }, - js.configs.recommended, + baseRecommendedConfig, // --- Config & Script Files (.js, .mjs, .cjs) --- { @@ -32,6 +95,47 @@ export default [ }, }, + // --- TypeScript Source Files (.ts, .tsx) --- + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: parserTypeScript, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + plugins: { + '@typescript-eslint': pluginTypeScript, + }, + rules: { + ...pluginTypeScript.configs.recommended.rules, + 'no-unused-vars': 'off', + 'no-undef': 'off', + 'no-await-in-loop': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/unbound-method': 'off', + }, + }, + // --- Vue Component Files (.vue) --- { files: ['**/*.vue'], @@ -41,7 +145,6 @@ export default [ ecmaVersion: 'latest', sourceType: 'module', parser: parserTypeScript, - project: './tsconfig.json', extraFileExtensions: ['.vue'], }, globals: { @@ -71,6 +174,14 @@ export default [ }, }, + { + files: ['**/*.d.ts'], + rules: { + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + { files: ['src/partials/EducationPartial.vue', 'src/partials/RecommendationPartial.vue', 'src/pages/PostPage.vue'], rules: { diff --git a/src/css/style.css b/src/css/style.css index 8eb71d1b..2fcc39f1 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -8,9 +8,9 @@ /* ------------------------------------------------------- tailwindcss ---------------------------------------------- */ /* ------------------------------------------------------------------------------------------------------------------ */ @layer theme, base, components, utilities; -@import "tailwindcss/theme.css" layer(theme); -@import "tailwindcss/preflight.css" layer(base); -@import "tailwindcss/utilities.css" layer(utilities); +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/preflight.css' layer(base); +@import 'tailwindcss/utilities.css' layer(utilities); /* ------------------------------------------------------------------------------------------------------------------ */ /* ------------------------------------------------------- customised ----------------------------------------------- */ @@ -22,7 +22,7 @@ @import '@css/support/blog.css'; @plugin "@tailwindcss/forms" { - strategy: base; + strategy: base; } @custom-variant dark (&:is(.dark *)); @@ -73,15 +73,15 @@ --tracking-wider: 0.02em; --tracking-widest: 0.4em; - --color-fuchsia-50: oklch(0.977 0.017 320.058); - --color-fuchsia-100: oklch(0.952 0.037 318.852); - --color-fuchsia-200: oklch(0.903 0.076 319.62); - --color-fuchsia-300: oklch(0.833 0.145 321.434); - --color-fuchsia-400: oklch(0.74 0.238 322.16); - --color-fuchsia-500: oklch(0.667 0.295 322.15); - --color-fuchsia-600: oklch(0.591 0.293 322.896); - --color-fuchsia-700: oklch(0.518 0.253 323.949); - --color-fuchsia-800: oklch(0.452 0.211 324.591); - --color-fuchsia-900: oklch(0.401 0.17 325.612); - --color-fuchsia-950: oklch(0.293 0.136 325.661); + --color-fuchsia-50: oklch(0.977 0.017 320.058); + --color-fuchsia-100: oklch(0.952 0.037 318.852); + --color-fuchsia-200: oklch(0.903 0.076 319.62); + --color-fuchsia-300: oklch(0.833 0.145 321.434); + --color-fuchsia-400: oklch(0.74 0.238 322.16); + --color-fuchsia-500: oklch(0.667 0.295 322.15); + --color-fuchsia-600: oklch(0.591 0.293 322.896); + --color-fuchsia-700: oklch(0.518 0.253 323.949); + --color-fuchsia-800: oklch(0.452 0.211 324.591); + --color-fuchsia-900: oklch(0.401 0.17 325.612); + --color-fuchsia-950: oklch(0.293 0.136 325.661); } diff --git a/src/css/support/blog.css b/src/css/support/blog.css index 6c637d5d..c1533dae 100644 --- a/src/css/support/blog.css +++ b/src/css/support/blog.css @@ -1,73 +1,73 @@ .blog-link { - @apply text-fuchsia-500 font-medium; - @apply hover:cursor-pointer hover:underline; - @apply dark:text-teal-500 dark:hover:text-teal-600; + @apply text-fuchsia-500 font-medium; + @apply hover:cursor-pointer hover:underline; + @apply dark:text-teal-500 dark:hover:text-teal-600; } .blog-h1 { - @apply font-aspekta mb-5; - @apply text-slate-700 dark:text-slate-300; + @apply font-aspekta mb-5; + @apply text-slate-700 dark:text-slate-300; } .blog-fun-title-word-highlight { - @apply inline-flex relative text-fuchsia-500 dark:text-teal-600 before:absolute before:inset-0; - @apply before:bg-slate-300 dark:before:bg-slate-600 before:opacity-30 before:-z-10 before:-rotate-2; - @apply before:translate-y-1/4; + @apply inline-flex relative text-fuchsia-500 dark:text-teal-600 before:absolute before:inset-0; + @apply before:bg-slate-300 dark:before:bg-slate-600 before:opacity-30 before:-z-10 before:-rotate-2; + @apply before:translate-y-1/4; } /* --- side nav --- */ .blog-side-nav-router-link-a { - @apply w-full flex items-center justify-center relative after:absolute after:w-0.5; - @apply after:right-0 after:top-0 after:bottom-0; + @apply w-full flex items-center justify-center relative after:absolute after:w-0.5; + @apply after:right-0 after:top-0 after:bottom-0; } .blog-side-nav-router-link-a-resting { - @apply text-slate-200 hover:text-fuchsia-500 hover:after:bg-fuchsia-600; - @apply dark:text-slate-700 dark:hover:text-teal-600 dark:hover:after:bg-teal-600; + @apply text-slate-200 hover:text-fuchsia-500 hover:after:bg-fuchsia-600; + @apply dark:text-slate-700 dark:hover:text-teal-600 dark:hover:after:bg-teal-600; } .blog-side-nav-router-link-a-active { - @apply text-fuchsia-500 after:bg-fuchsia-500 hover:after:bg-fuchsia-600 hover:text-fuchsia-600; - @apply dark:text-teal-600 dark:after:bg-teal-600 dark:hover:after:bg-slate-500; + @apply text-fuchsia-500 after:bg-fuchsia-500 hover:after:bg-fuchsia-600 hover:text-fuchsia-600; + @apply dark:text-teal-600 dark:after:bg-teal-600 dark:hover:after:bg-slate-500; } /* --- footer --- */ .blog-footer-social-li { - @apply flex justify-center items-center h-10 w-10 bg-gray-400 border border-gray-400 rounded-md; - @apply transition duration-150 ease-in-out hover:bg-fuchsia-400 hover:border-fuchsia-400 cursor-pointer; - @apply dark:bg-linear-to-t dark:bg-slate-900 dark:border-slate-800 dark:from-slate-800 dark:to-slate-800/30; - @apply dark:hover:bg-linear-to-b; + @apply flex justify-center items-center h-10 w-10 bg-gray-400 border border-gray-400 rounded-md; + @apply transition duration-150 ease-in-out hover:bg-fuchsia-400 hover:border-fuchsia-400 cursor-pointer; + @apply dark:bg-linear-to-t dark:bg-slate-900 dark:border-slate-800 dark:from-slate-800 dark:to-slate-800/30; + @apply dark:hover:bg-linear-to-b; } .blog-widgets-btn-sm { - @apply w-full text-slate-100 bg-slate-700 hover:bg-slate-800 cursor-pointer; - @apply dark:bg-teal-600 dark:hover:bg-teal-700; + @apply w-full text-slate-100 bg-slate-700 hover:bg-slate-800 cursor-pointer; + @apply dark:bg-teal-600 dark:hover:bg-teal-700; } .blog-widgets-subscriber-avatar { - @apply rounded-full border-2 border-white dark:border-slate-800 box-content; + @apply rounded-full border-2 border-white dark:border-slate-800 box-content; } /* --- header --- */ .blog-header-search-icon { - @apply fill-fuchsia-500; - @apply dark:fill-teal-500; + @apply fill-fuchsia-500; + @apply dark:fill-teal-500; } /* --- social --- */ .blog-widgets-social-links { - @apply flex text-sm font-medium text-zinc-800 transition hover:text-fuchsia-500; - @apply dark:text-zinc-200 dark:hover:text-teal-500; + @apply flex text-sm font-medium text-zinc-800 transition hover:text-fuchsia-500; + @apply dark:text-zinc-200 dark:hover:text-teal-500; } .blog-widgets-social-svg { - @apply h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-fuchsia-500; - @apply dark:group-hover:fill-teal-500; + @apply h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-fuchsia-500; + @apply dark:group-hover:fill-teal-500; } /* --- markdown content --- */ .post-markdown { - @apply text-base leading-relaxed text-slate-500 dark:text-slate-300; + @apply text-base leading-relaxed text-slate-500 dark:text-slate-300; } .post-markdown h1, @@ -76,31 +76,31 @@ .post-markdown h4, .post-markdown h5, .post-markdown h6 { - @apply font-aspekta font-semibold text-slate-700 dark:text-slate-100; + @apply font-aspekta font-semibold text-slate-700 dark:text-slate-100; } .post-markdown h1 { - @apply text-4xl mt-10 mb-4; + @apply text-4xl mt-10 mb-4; } .post-markdown h2 { - @apply text-3xl mt-10 mb-4; + @apply text-3xl mt-10 mb-4; } .post-markdown h3 { - @apply text-2xl mt-8 mb-3; + @apply text-2xl mt-8 mb-3; } .post-markdown h4 { - @apply text-xl mt-6 mb-2; + @apply text-xl mt-6 mb-2; } .post-markdown h5 { - @apply text-lg mt-6 mb-2; + @apply text-lg mt-6 mb-2; } .post-markdown h6 { - @apply text-base uppercase tracking-wide text-slate-400 dark:text-slate-500 mt-6 mb-2; + @apply text-base uppercase tracking-wide text-slate-400 dark:text-slate-500 mt-6 mb-2; } .post-markdown p, @@ -110,94 +110,98 @@ .post-markdown pre, .post-markdown table, .post-markdown hr { - @apply mt-6; + @apply mt-6; } .post-markdown > *:first-child { - @apply mt-0; + @apply mt-0; } .post-markdown a { - @apply text-fuchsia-500 font-medium underline underline-offset-4 decoration-1 hover:text-fuchsia-600 transition; - @apply dark:text-teal-500 dark:hover:text-teal-400; + @apply text-fuchsia-500 font-medium underline underline-offset-4 decoration-1 hover:text-fuchsia-600 transition; + @apply dark:text-teal-500 dark:hover:text-teal-400; } .post-markdown strong { - @apply text-slate-700 dark:text-slate-100; + @apply text-slate-700 dark:text-slate-100; } .post-markdown em { - @apply italic; + @apply italic; } .post-markdown ul { - @apply list-disc pl-6 space-y-2; + @apply list-disc pl-6 space-y-2; } .post-markdown ol { - @apply list-decimal pl-6 space-y-2; + @apply list-decimal pl-6 space-y-2; } .post-markdown li > ul, .post-markdown li > ol { - @apply mt-2; + @apply mt-2; } .post-markdown blockquote { - @apply border-l-4 border-fuchsia-500 dark:border-teal-600 pl-6 py-3 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-800/50 rounded-r-xl; + @apply border-l-4 border-fuchsia-500 dark:border-teal-600 pl-6 py-3 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-800/50 rounded-r-xl; } .post-markdown blockquote > :first-child { - @apply mt-1; + @apply mt-1; } .post-markdown blockquote > :last-child { - @apply mb-1; + @apply mb-1; } .post-markdown hr { - @apply border-slate-200 dark:border-slate-700; + @apply border-slate-200 dark:border-slate-700; +} + +.project-card-content { + @apply flex flex-col h-full min-h-[220px]; } .post-markdown table { - @apply w-full text-sm border border-slate-200 dark:border-slate-700; - border-collapse: collapse; + @apply w-full text-sm border border-slate-200 dark:border-slate-700; + border-collapse: collapse; } @media (max-width: 768px) { - .post-markdown table { - display: block; - overflow-x: auto; - } + .post-markdown table { + display: block; + overflow-x: auto; + } } .post-markdown thead { - @apply bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-200; + @apply bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-200; } .post-markdown th, .post-markdown td { - @apply border border-slate-200 dark:border-slate-700 px-4 py-3 align-top text-left; + @apply border border-slate-200 dark:border-slate-700 px-4 py-3 align-top text-left; } .post-markdown pre { - --scrollbar-track: var(--custom-scrollbar-track); - --scrollbar-thumb: var(--custom-scrollbar-thumb); - --scrollbar-thumb-hover: var(--custom-scrollbar-thumb-hover); - @apply bg-slate-900 text-slate-100 dark:bg-slate-900/90 dark:text-slate-100 rounded-2xl p-6 overflow-x-auto text-sm; - scrollbar-width: thin; /* Firefox */ - scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); + --scrollbar-track: var(--custom-scrollbar-track); + --scrollbar-thumb: var(--custom-scrollbar-thumb); + --scrollbar-thumb-hover: var(--custom-scrollbar-thumb-hover); + @apply bg-slate-900 text-slate-100 dark:bg-slate-900/90 dark:text-slate-100 rounded-2xl p-6 overflow-x-auto text-sm; + scrollbar-width: thin; /* Firefox */ + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); } .post-markdown code { - @apply font-pt-mono text-sm bg-slate-900/10 text-slate-700 dark:bg-slate-700/70 dark:text-slate-100 rounded px-2 py-1; + @apply font-pt-mono text-sm bg-slate-900/10 text-slate-700 dark:bg-slate-700/70 dark:text-slate-100 rounded px-2 py-1; } .post-markdown pre code { - @apply bg-transparent px-0 py-0 text-inherit; + @apply bg-transparent px-0 py-0 text-inherit; } .post-markdown img { - @apply shadow-sm dark:shadow-none rounded-md; - max-width: 100%; + @apply shadow-sm dark:shadow-none rounded-md; + max-width: 100%; } diff --git a/src/css/support/theme.css b/src/css/support/theme.css index e11c1560..0617d582 100644 --- a/src/css/support/theme.css +++ b/src/css/support/theme.css @@ -48,49 +48,48 @@ width 0.1s ease-out; } - :root { - --custom-scrollbar-track: var(--color-gray-100); - --custom-scrollbar-thumb: var(--color-fuchsia-500); - --custom-scrollbar-thumb-hover: var(--color-fuchsia-400); + --custom-scrollbar-track: var(--color-gray-100); + --custom-scrollbar-thumb: var(--color-fuchsia-500); + --custom-scrollbar-thumb-hover: var(--color-fuchsia-400); } .dark { - --custom-scrollbar-track: var(--color-slate-800); - --custom-scrollbar-thumb: var(--color-teal-500); - --custom-scrollbar-thumb-hover: var(--color-teal-400); + --custom-scrollbar-track: var(--color-slate-800); + --custom-scrollbar-thumb: var(--color-teal-500); + --custom-scrollbar-thumb-hover: var(--color-teal-400); } .custom-scrollbar { - --scrollbar-track: var(--custom-scrollbar-track); - --scrollbar-thumb: var(--custom-scrollbar-thumb); - --scrollbar-thumb-hover: var(--custom-scrollbar-thumb-hover); + --scrollbar-track: var(--custom-scrollbar-track); + --scrollbar-thumb: var(--custom-scrollbar-thumb); + --scrollbar-thumb-hover: var(--custom-scrollbar-thumb-hover); } .custom-scrollbar, .post-markdown pre { - scrollbar-width: thin; /* Firefox */ - scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); + scrollbar-width: thin; /* Firefox */ + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); } .custom-scrollbar::-webkit-scrollbar, .post-markdown pre::-webkit-scrollbar { - width: 8px; + width: 8px; } .custom-scrollbar::-webkit-scrollbar-track, .post-markdown pre::-webkit-scrollbar-track { - background-color: var(--scrollbar-track); - border-radius: 10px; + background-color: var(--scrollbar-track); + border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb, .post-markdown pre::-webkit-scrollbar-thumb { - background-color: var(--scrollbar-thumb); - border-radius: 10px; + background-color: var(--scrollbar-thumb); + border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover, .post-markdown pre::-webkit-scrollbar-thumb:hover { - background-color: var(--scrollbar-thumb-hover); + background-color: var(--scrollbar-thumb-hover); } diff --git a/src/pages/AboutPage.vue b/src/pages/AboutPage.vue index 489ba43b..237c1d6a 100644 --- a/src/pages/AboutPage.vue +++ b/src/pages/AboutPage.vue @@ -68,7 +68,8 @@ @@ -88,6 +89,7 @@ import HeaderPartial from '@partials/HeaderPartial.vue'; import SideNavPartial from '@partials/SideNavPartial.vue'; import WidgetSocialPartial from '@partials/WidgetSocialPartial.vue'; import WidgetSkillsPartial from '@partials/WidgetSkillsPartial.vue'; +import WidgetSkillsSkeletonPartial from '@partials/WidgetSkillsSkeletonPartial.vue'; import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_LD } from '@/support/seo'; import { useApiStore } from '@api/store.ts'; @@ -97,6 +99,7 @@ import type { ProfileResponse } from '@api/response/index.ts'; const apiStore = useApiStore(); const nickname = ref('Gus'); const profile = ref(null); +const isLoadingProfile = ref(true); const aboutPicture = computed(() => { return AboutPicture; @@ -137,6 +140,8 @@ onMounted(async () => { } } catch (error) { debugError(error); + } finally { + isLoadingProfile.value = false; } }); diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue index 2efe7e48..da90b22d 100644 --- a/src/pages/HomePage.vue +++ b/src/pages/HomePage.vue @@ -26,7 +26,8 @@ @@ -48,6 +49,7 @@ import WidgetSkillsPartial from '@partials/WidgetSkillsPartial.vue'; import ArticlesListPartial from '@partials/ArticlesListPartial.vue'; import WidgetSponsorPartial from '@partials/WidgetSponsorPartial.vue'; import FeaturedProjectsPartial from '@partials/FeaturedProjectsPartial.vue'; +import WidgetSkillsSkeletonPartial from '@partials/WidgetSkillsSkeletonPartial.vue'; import { onMounted, ref } from 'vue'; import { useApiStore } from '@api/store.ts'; @@ -57,6 +59,7 @@ import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_ const apiStore = useApiStore(); const profile = ref(null); +const isLoadingProfile = ref(true); useSeo({ title: 'Home', @@ -86,6 +89,8 @@ onMounted(async () => { } } catch (error) { debugError(error); + } finally { + isLoadingProfile.value = false; } }); diff --git a/src/pages/ProjectsPage.vue b/src/pages/ProjectsPage.vue index 1171479c..dc55b0f7 100644 --- a/src/pages/ProjectsPage.vue +++ b/src/pages/ProjectsPage.vue @@ -31,13 +31,12 @@

Open Source / Client Projects

-