├── .prettierignore ├── .tool-versions ├── public ├── robots.txt ├── favicon.ico ├── favicon.png ├── _headers ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── app.webmanifest └── safari-pinned-tab.svg ├── .npmrc ├── src ├── layouts │ ├── simple.vue │ └── default.vue ├── assets │ ├── clip.mp4 │ ├── img │ │ ├── me.png │ │ ├── meme.png │ │ ├── profile.jpg │ │ └── water_drop.png │ ├── clips │ │ └── flicker.mp4 │ ├── posts │ │ └── book_cover.jpeg │ └── styles │ │ ├── utility.css │ │ ├── reset.css │ │ ├── markdown.css │ │ └── global.css ├── modules │ ├── pwa.js │ ├── gtag.js │ ├── urql.js │ └── firebase.js ├── config │ └── siteconfig.json ├── components │ ├── svg │ │ ├── CircleFillSVG.vue │ │ ├── BinaryTextSVG.vue │ │ ├── PagesSVG.vue │ │ ├── VueSVG.vue │ │ ├── LampOffSVG.vue │ │ ├── PullRequestSVG.vue │ │ ├── GridsomeSVG.vue │ │ ├── SpotLightSVG.vue │ │ ├── LampOnSVG.vue │ │ └── NetlifyLogoSVG.vue │ ├── demos │ │ ├── TwoCols.vue │ │ ├── DemoWrapper.vue │ │ ├── AnimatedBook.vue │ │ ├── AnimatedButton.vue │ │ ├── AnimatedBlocks.vue │ │ ├── ColorBox.vue │ │ ├── AnimatedBox.vue │ │ ├── ContainerCard.vue │ │ ├── BreadcrumbStyle.vue │ │ ├── ResponsiveProfileCard.vue │ │ ├── ContainerUnits.vue │ │ ├── SmartGrid.vue │ │ ├── CustomThemes.vue │ │ ├── ColorMix.vue │ │ ├── AnimatedSVG.vue │ │ ├── GeneratePallete.vue │ │ ├── Resizable.vue │ │ └── CSSPreview.vue │ ├── article │ │ ├── CustomList.vue │ │ ├── ArticlesHero.vue │ │ ├── MeteorBackground.vue │ │ ├── ArticleControls.vue │ │ ├── GalaxyBackground.vue │ │ └── ArticleHeading.vue │ ├── ui │ │ ├── TagGroup.vue │ │ ├── SkipLink.vue │ │ ├── Clock.vue │ │ ├── HamburgerMenu.vue │ │ ├── ScrollIndicator.vue │ │ ├── InteractiveWrapper.vue │ │ ├── ShareButton.vue │ │ ├── ButtonGroup.vue │ │ ├── Tag.vue │ │ ├── ImgLazy.vue │ │ ├── ThemeSwitcher.vue │ │ ├── RippleButton.vue │ │ ├── Button.vue │ │ └── CoffeeButton.vue │ ├── forms │ │ ├── BaseSelect.vue │ │ └── BaseRange.vue │ ├── github │ │ ├── GithubCardEmpty.vue │ │ ├── GithubProjects.vue │ │ └── GithubRepoCard.vue │ ├── posts │ │ ├── PostsList.vue │ │ └── PostPreview.vue │ ├── projects │ │ ├── ProjectsGrid.vue │ │ └── ProjectCard.vue │ ├── base │ │ └── AppLink.vue │ ├── layout │ │ ├── Footer.vue │ │ ├── MobileNavbar.vue │ │ └── Navbar.vue │ └── hero │ │ ├── HeroAvatar.vue │ │ └── HeroSection.vue ├── App.vue ├── api │ └── jokes │ │ └── jokes.mjs ├── main.js ├── composables │ ├── useCoffeeStore.js │ ├── useArticles.js │ ├── useContributions.js │ └── useCoffeeControl.js └── pages │ ├── index.vue │ ├── tags │ └── [tag].vue │ ├── articles │ └── index.vue │ ├── now.md │ └── about-me.vue ├── jsconfig.json ├── .prettierrc ├── lib ├── modes.js ├── formatters │ └── formatPoints.js ├── handling │ └── debounce.js ├── visitor │ └── getVisitorId.js ├── adapters │ └── DevToArticleAdapter.js └── md │ ├── copy-code.js │ └── index.js ├── .editorconfig ├── .env.example ├── eslint.config.js ├── netlify.toml ├── postcss.config.cjs ├── .gitignore ├── .stylelintrc.json ├── README.md ├── .github └── dependabot.yml ├── index.html ├── scripts └── create-draft.js ├── package.json ├── auto-imports.d.ts ├── vite.config.js ├── content ├── articles │ └── how-to-keep-up.md └── projects │ └── projects.json └── components.d.ts /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 23.10.0 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /src/layouts/simple.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*", "./lib/**/*", "./content/**/*"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /assets/* 2 | cache-control: max-age=31536000 3 | cache-control: immutable 4 | -------------------------------------------------------------------------------- /src/assets/clip.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/clip.mp4 -------------------------------------------------------------------------------- /src/assets/img/me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/img/me.png -------------------------------------------------------------------------------- /src/assets/img/meme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/img/meme.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/clips/flicker.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/clips/flicker.mp4 -------------------------------------------------------------------------------- /src/assets/img/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/img/profile.jpg -------------------------------------------------------------------------------- /src/assets/img/water_drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/img/water_drop.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/posts/book_cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeferson-sb/portfolio/HEAD/src/assets/posts/book_cover.jpeg -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true 6 | } 7 | -------------------------------------------------------------------------------- /lib/modes.js: -------------------------------------------------------------------------------- 1 | export const isProd = import.meta.env.PROD 2 | export const isDev = import.meta.env.DEV 3 | export const isSSR = import.meta.env.SSR -------------------------------------------------------------------------------- /lib/formatters/formatPoints.js: -------------------------------------------------------------------------------- 1 | export const formatPoints = (n) => 2 | Intl.NumberFormat('en-US', { 3 | notation: 'compact', 4 | compactDisplay: 'short', 5 | }).format(n) 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DEVTO_API_KEY= 2 | FIREBASE_API_KEY= 3 | FIREBASE_APP_ID= 4 | FIREBASE_MEASUREMENT_ID= 5 | FIREBASE_MESSAGE_SENDER= 6 | FIREBASE_PROJECT_ID= 7 | GITHUB_TOKEN= 8 | GOOGLE_ANALYTICS_ID= 9 | GOOGLE_SEARCH_CONSOLE_TOKEN= 10 | -------------------------------------------------------------------------------- /lib/handling/debounce.js: -------------------------------------------------------------------------------- 1 | const debounce = (fn, ms = 0) => { 2 | let timeoutId; 3 | return function (...args) { 4 | clearTimeout(timeoutId); 5 | timeoutId = setTimeout(() => fn.apply(this, args), ms); 6 | }; 7 | }; 8 | 9 | export default debounce -------------------------------------------------------------------------------- /lib/visitor/getVisitorId.js: -------------------------------------------------------------------------------- 1 | import { load as fingerprint } from '@fingerprintjs/fingerprintjs' 2 | 3 | export const getVisitorId = async () => { 4 | const value = await fingerprint() 5 | const { visitorId } = await value.get() 6 | return visitorId 7 | } -------------------------------------------------------------------------------- /src/modules/pwa.js: -------------------------------------------------------------------------------- 1 | export const install = ({ isClient, router }) => { 2 | if (!isClient) return 3 | 4 | router.isReady().then(async () => { 5 | const { registerSW } = await import('virtual:pwa-register') 6 | registerSW({ immediate: true }) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/config/siteconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteName": "Software Enginner", 3 | "siteUrl": "https://jefersonsilva.me", 4 | "description": "I am Software Engineer that crafts things for the Web, heavy focused on front-end development enthusiastic and fascinated on UI design" 5 | } 6 | -------------------------------------------------------------------------------- /src/components/svg/CircleFillSVG.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/modules/gtag.js: -------------------------------------------------------------------------------- 1 | import { createGtag } from 'vue-gtag' 2 | 3 | const gtag = createGtag({ 4 | tagId: import.meta.env.VITE_GOOGLE_ANALYTICS_ID, 5 | }) 6 | 7 | export const install = ({ isClient, app }) => { 8 | if (!isClient) return 9 | 10 | app.use(gtag) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/demos/TwoCols.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginVue from 'eslint-plugin-vue' 2 | import eslintConfigPrettier from 'eslint-config-prettier'; 3 | 4 | export default [ 5 | ...pluginVue.configs['flat/recommended'], 6 | { 7 | rules: { 8 | ...eslintConfigPrettier.rules, 9 | } 10 | } 11 | ] -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "dist" 4 | functions = "src/api" 5 | 6 | [[redirects]] 7 | from = "/*" 8 | to = "/index.html" 9 | status = 200 10 | 11 | [[redirects]] 12 | from = '/api/*' 13 | to = '/.netlify/functions/:splat' 14 | status = 200 15 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #f4f4f4 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/demos/DemoWrapper.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env'); 2 | const postcssGlobalData = require('@csstools/postcss-global-data'); 3 | 4 | module.exports = { 5 | plugins: [ 6 | postcssGlobalData({ 7 | files: [ 8 | './src/assets/styles/global.css' 9 | ] 10 | }), 11 | postcssPresetEnv() 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .cache 11 | .DS_Store 12 | src/.temp 13 | node_modules 14 | dist 15 | 16 | .env 17 | .idea 18 | .vscode 19 | # Local Netlify folder 20 | .netlify 21 | draft/ 22 | draft/* 23 | *:Zone.Identifier 24 | 25 | .vite-ssg-temp 26 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /src/components/article/CustomList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/components/svg/BinaryTextSVG.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-recommended-vue" 5 | ], 6 | "rules": { 7 | "declaration-block-no-redundant-longhand-properties": null, 8 | "custom-property-empty-line-before": null, 9 | "selector-class-pattern": null, 10 | "media-feature-range-notation": "prefix", 11 | "comment-empty-line-before": "always", 12 | "declaration-no-important": true, 13 | "max-nesting-depth": 3 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/TagGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/components/ui/SkipLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/modules/urql.js: -------------------------------------------------------------------------------- 1 | import * as urql from '@urql/vue' 2 | import { cacheExchange, fetchExchange } from '@urql/core' 3 | 4 | export const install = ({ isClient, app }) => { 5 | // if (!isClient) return 6 | 7 | app.use(urql, { 8 | url: 'https://api.github.com/graphql', 9 | fetchOptions: () => { 10 | const token = import.meta.env.VITE_GITHUB_TOKEN 11 | return { 12 | headers: { authorization: token ? `Bearer ${token}` : '' }, 13 | } 14 | }, 15 | exchanges: [cacheExchange, fetchExchange], 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/api/jokes/jokes.mjs: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | try { 3 | const response = await fetch('https://v2.jokeapi.dev/joke/programming') 4 | const data = await response.json() 5 | const ip = 6 | context.ip || 7 | req.headers['x-forwarded-for'] || 8 | req.socket.remoteAddress || 9 | null 10 | 11 | return Response.json({ joke: data.joke || data.delivery, ip }, { status: 200 }) 12 | } catch (error) { 13 | return { 14 | statusCode: 500, 15 | body: { error: 'Oof.. Something went bad' }, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/forms/BaseSelect.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /lib/adapters/DevToArticleAdapter.js: -------------------------------------------------------------------------------- 1 | class DevToArticleAdapter { 2 | static transformFrontMatter(source) { 3 | return { 4 | id: source.id, 5 | title: source.title, 6 | slug: source.slug, 7 | published_at: source.published_at, 8 | tags: source.tag_list, 9 | excerpt: source.description, 10 | crosspostedOn: 'Dev.to', 11 | crosspostLink: source.url, 12 | url: source.url, 13 | path: `/articles/${source.slug}`, 14 | canonicalUrl: source.canonical_url, 15 | cover_image: source.cover_image, 16 | } 17 | } 18 | } 19 | 20 | module.exports = DevToArticleAdapter 21 | -------------------------------------------------------------------------------- /src/components/svg/PagesSVG.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/styles/utility.css: -------------------------------------------------------------------------------- 1 | @layer utility { 2 | .container { 3 | --max-w: 1110px; 4 | 5 | margin-inline: auto; 6 | width: calc(100% - 2rem); 7 | max-width: var(--max-w); 8 | display: flex; 9 | } 10 | 11 | .overflow--hidden { 12 | overflow: hidden; 13 | } 14 | 15 | .sr-only:not(:focus, :active) { 16 | clip: rect(0 0 0 0); 17 | clip-path: inset(50%); 18 | height: 1px; 19 | overflow: hidden; 20 | position: absolute; 21 | white-space: nowrap; 22 | width: 1px; 23 | } 24 | 25 | @media screen and (min-width: 26.625em) { 26 | .is-mobile-only { 27 | display: none; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/demos/AnimatedBook.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/github/GithubCardEmpty.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /src/components/ui/Clock.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/demos/AnimatedButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | -------------------------------------------------------------------------------- /src/components/ui/HamburgerMenu.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { ViteSSG } from 'vite-ssg' 2 | import generatedRoutes from 'virtual:generated-pages' 3 | import { setupLayouts } from 'virtual:generated-layouts' 4 | import App from './App.vue' 5 | 6 | import '@/assets/styles/reset.css' 7 | import '@/assets/styles/global.css' 8 | import '@/assets/styles/markdown.css' 9 | import '@/assets/styles/utility.css' 10 | 11 | const routes = setupLayouts(generatedRoutes) 12 | 13 | console.log( 14 | `%c⚠️ I hope you know what you're doing! :p`, 15 | 'color: #c9a43e; background: #303030; border-radius: 4px; padding: 0.25rem; font-family: cursive; font-style: italic; font-weight: 700;' 16 | ) 17 | 18 | export const createApp = ViteSSG( 19 | App, 20 | { routes, base: import.meta.env.BASE_URL }, 21 | (ctx) => { 22 | // install all modules under `modules/` 23 | Object.values(import.meta.glob('./modules/*.js', { eager: true })).forEach( 24 | (i) => i.install?.(ctx) 25 | ) 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /public/app.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en-US", 3 | "name": "Jeferson's Portfolio", 4 | "short_name": "JB", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "description": "I'm a Software Engineer that crafts things for the Web, heavy focused on front-end development enthusiastic and fascinated on UI design", 8 | "icons": [ 9 | { 10 | "src": "./android-chrome-192x192.png", 11 | "sizes": "192x192", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "./android-chrome-512x512.png", 16 | "sizes": "512x512", 17 | "type": "image/png" 18 | } 19 | ], 20 | "orientation": "landscape", 21 | "theme_color": "#1A1C1F", 22 | "background_color": "#1ABBD1", 23 | "shortcuts": [ 24 | { 25 | "name": "Read articles", 26 | "url": "/articles", 27 | "icons": [ 28 | { 29 | "src": "./android-chrome-192x192.png", 30 | "sizes": "192x192" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/posts/PostsList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /src/components/svg/VueSVG.vue: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/components/projects/ProjectsGrid.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 38 | -------------------------------------------------------------------------------- /src/components/demos/AnimatedBlocks.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 51 | -------------------------------------------------------------------------------- /src/components/demos/ColorBox.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /src/components/ui/ScrollIndicator.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

My Portfolio

4 |

Developed using Vue + Vite ♥

5 |

6 | 7 | [![Netlify Status](https://api.netlify.com/api/v1/badges/3cbacfe5-8f5b-4898-b627-645b4a1f472c/deploy-status)](https://app.netlify.com/sites/jefersonsilva/deploys) 8 | 9 | ## Getting started 10 | 11 | > [!IMPORTANT] 12 | > I appreciate your interest in adapting or modifying this project to fit your needs. Just a gentle reminder, if you do choose to fork or make changes, please ensure proper credit is given for the original work. Thank you! 13 | 14 | ### Clone repository 15 | 16 | ```bash 17 | git clone https://github.com/jeferson-sb/portfolio.git 18 | ``` 19 | 20 | ### Install dependencies 21 | 22 | ```sh 23 | cd portfolio 24 | pnpm install 25 | ``` 26 | 27 | ### Start local dev server 28 | 29 | ```sh 30 | pnpm dev 31 | ``` 32 | 33 | ### Linting 34 | 35 | ```sh 36 | pnpm lint:js 37 | pnpm lint:css 38 | ``` 39 | 40 | ### Formatting 41 | 42 | ```sh 43 | pnpm format 44 | ``` 45 | -------------------------------------------------------------------------------- /src/modules/firebase.js: -------------------------------------------------------------------------------- 1 | import { VueFire, VueFireAppCheck } from 'vuefire' 2 | import { initializeApp } from 'firebase/app' 3 | import { getFirestore } from 'firebase/firestore' 4 | import { ReCaptchaV3Provider } from 'firebase/app-check' 5 | 6 | // Initialize Firebase 7 | export const firebaseApp = initializeApp({ 8 | appId: "1:545804876347:web:22693f5fb3da8f3ebbba1e", 9 | apiKey: "AIzaSyBmwcdM6SlakmUiBIf_hGrKXDv-5U_WbSU", 10 | authDomain: "portfolio-d3c7c.firebaseapp.com", 11 | projectId: "portfolio-d3c7c", 12 | storageBucket: "portfolio-d3c7c.appspot.com", 13 | databaseURL: 'https://portfolio-d3c7c-default-rtdb.firebaseio.com/', 14 | }) 15 | 16 | export const db = getFirestore(firebaseApp) 17 | 18 | export const install = ({ isClient, app }) => { 19 | app.use(VueFire, { 20 | firebaseApp, 21 | modules: [ 22 | VueFireAppCheck({ 23 | provider: new ReCaptchaV3Provider(import.meta.env.VITE_RECAPTCHA), 24 | debug: process.env.NODE_ENV !== 'production', 25 | isTokenAutoRefreshEnabled: true, 26 | }), 27 | ] 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/composables/useCoffeeStore.js: -------------------------------------------------------------------------------- 1 | import { useCollection } from 'vuefire' 2 | import { collection, doc, setDoc, Timestamp } from 'firebase/firestore' 3 | 4 | import { db } from '@/modules/firebase' 5 | 6 | export function useCoffeeStore() { 7 | const coffeeRef = collection(db, 'coffee') 8 | const coffee = useCollection(coffeeRef, { once: true }) 9 | 10 | const addCoffeePoints = async ({ articleId, points, visitorId }) => { 11 | if (!articleId || !visitorId) return 12 | 13 | try { 14 | const userDoc = doc(coffeeRef, visitorId) 15 | const userCups = coffee.value.find((cup) => cup.id === visitorId) 16 | const articles = userCups?.articles || {} 17 | 18 | await setDoc(userDoc, { 19 | articles: { 20 | ...articles, 21 | [articleId]: { 22 | points, 23 | date: Timestamp.fromDate(new Date()), 24 | }, 25 | }, 26 | }) 27 | } catch (error) { 28 | console.error('Failed to set coffee points for visitor', error) 29 | } 30 | } 31 | 32 | return { coffee, addCoffeePoints } 33 | } 34 | -------------------------------------------------------------------------------- /lib/md/copy-code.js: -------------------------------------------------------------------------------- 1 | const wrap = (render) => (...args) => { 2 | return render.apply(this, args) 3 | .replace( 4 | '', 5 | `` 10 | ) 11 | } 12 | 13 | const copyPlugin = (md) => { 14 | md.renderer.rules.code_block = wrap(md.renderer.rules.code_block); 15 | md.renderer.rules.fence = wrap(md.renderer.rules.fence); 16 | }; 17 | 18 | export { copyPlugin } 19 | -------------------------------------------------------------------------------- /src/components/article/ArticlesHero.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/svg/LampOffSVG.vue: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | -------------------------------------------------------------------------------- /src/components/ui/InteractiveWrapper.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | 30 | 51 | -------------------------------------------------------------------------------- /src/components/ui/ShareButton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /src/components/article/MeteorBackground.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 53 | -------------------------------------------------------------------------------- /src/components/ui/ButtonGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | 31 | 64 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: eslint 10 | versions: 11 | - 7.18.0 12 | - 7.19.0 13 | - 7.20.0 14 | - 7.21.0 15 | - 7.22.0 16 | - 7.23.0 17 | - 7.24.0 18 | - dependency-name: eslint-plugin-vue 19 | versions: 20 | - 7.5.0 21 | - 7.6.0 22 | - 7.7.0 23 | - 7.8.0 24 | - dependency-name: eslint-config-prettier 25 | versions: 26 | - 7.2.0 27 | - 8.0.0 28 | - 8.1.0 29 | - dependency-name: stylelint-config-standard 30 | versions: 31 | - 21.0.0 32 | - dependency-name: stylelint 33 | versions: 34 | - 13.10.0 35 | - 13.11.0 36 | - 13.12.0 37 | - dependency-name: elliptic 38 | versions: 39 | - 6.5.4 40 | - dependency-name: eslint-plugin-prettier 41 | versions: 42 | - 3.3.1 43 | - dependency-name: "@fortawesome/free-brands-svg-icons" 44 | versions: 45 | - 5.15.2 46 | - dependency-name: "@fortawesome/free-solid-svg-icons" 47 | versions: 48 | - 5.15.2 49 | - dependency-name: "@fortawesome/fontawesome-svg-core" 50 | versions: 51 | - 1.2.34 52 | - dependency-name: "@fortawesome/vue-fontawesome" 53 | versions: 54 | - 2.0.2 55 | - dependency-name: vue-i18n 56 | versions: 57 | - 8.22.3 58 | -------------------------------------------------------------------------------- /src/composables/useArticles.js: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useRouter } from 'vue-router' 3 | 4 | export function useArticles(options) { 5 | const router = useRouter() 6 | const tag = options?.tag 7 | 8 | const articles = computed(() => { 9 | return router 10 | .getRoutes() 11 | .filter((route) => { 12 | const isArticle = route.path.startsWith('/articles/') && route.name 13 | const tags = route.meta?.frontmatter?.tags 14 | 15 | if (tag) { 16 | const normalizedTags = tags ? tags.toLowerCase() : '' 17 | const matchTag = normalizedTags.indexOf(tag.toLowerCase()) !== -1 18 | 19 | return isArticle && matchTag 20 | } 21 | 22 | return isArticle 23 | }) 24 | .sort((a, b) => 25 | a.meta.frontmatter.published_at < b.meta.frontmatter.published_at 26 | ? 1 27 | : -1 28 | ) 29 | .map((article) => ({ 30 | id: article.meta?.frontmatter?.id, 31 | articleId: article.name, 32 | title: article.meta?.frontmatter?.title, 33 | date: article.meta?.frontmatter?.published_at, 34 | path: article.path, 35 | excerpt: article.meta?.frontmatter?.excerpt, 36 | tags: article.meta?.frontmatter?.tags, 37 | })) 38 | }) 39 | 40 | if (tag && !articles.value.length) router.push('/404') 41 | 42 | return articles 43 | } 44 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 15 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/ui/Tag.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 29 | 30 | 62 | -------------------------------------------------------------------------------- /lib/md/index.js: -------------------------------------------------------------------------------- 1 | const { promises: fs } = require('fs') 2 | 3 | async function writeFileOn(filePath, content) { 4 | try { 5 | const fileStats = async (p) => { 6 | try { 7 | const stats = await fs.stat(p) 8 | return stats 9 | } catch (error) { 10 | return 11 | } 12 | } 13 | 14 | const stats = await fileStats(filePath) 15 | const fileExists = !!stats && Object.keys(stats).length > 0 16 | 17 | if (!fileExists) { 18 | fs.writeFile(filePath, content) 19 | console.log('📰 Writing file...', filePath) 20 | } else { 21 | console.error('⚠ File already exists: ', filePath) 22 | } 23 | } catch (error) { 24 | throw new Error('Unable to write markdown file', error) 25 | } 26 | } 27 | 28 | async function transformMarkdown(payload, adapter) { 29 | let transformedMd = '' 30 | const md = payload.body_markdown 31 | const content = md.replace(/^---$.*^---$/ms, '').trim() 32 | const delimiter = '---' 33 | const frontmatter = Object.entries(adapter.transformFrontMatter(payload)) 34 | 35 | transformedMd += `${delimiter}\n` 36 | 37 | // eslint-disable-next-line no-restricted-syntax 38 | for (const [k, v] of frontmatter) { 39 | transformedMd += `${k}: ${v}\n` 40 | } 41 | 42 | transformedMd += `${delimiter}\n` 43 | transformedMd += content 44 | return transformedMd 45 | } 46 | 47 | module.exports = { writeFileOn, transformMarkdown } 48 | -------------------------------------------------------------------------------- /src/components/github/GithubProjects.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 54 | -------------------------------------------------------------------------------- /src/pages/tags/[tag].vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | 24 | 56 | -------------------------------------------------------------------------------- /scripts/create-draft.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | import readline from 'node:readline/promises' 4 | 5 | const defaults = { 6 | id: Math.floor(Math.random() * 1e15).toString(36), 7 | published_at: new Date().toISOString(), 8 | excerpt: '', 9 | crosspostedOn: '', 10 | crosspostLink: '', 11 | } 12 | 13 | const slugify = (str) => 14 | str 15 | .toLowerCase() 16 | .trim() 17 | .replace(/[^\w\s-]/g, '') 18 | .replace(/[\s_-]+/g, '-') 19 | .replace(/^-+|-+$/g, '') 20 | 21 | const rl = readline.createInterface({ 22 | input: process.stdin, 23 | output: process.stdout, 24 | }) 25 | 26 | const title = await rl.question("What's the title of your post? ") 27 | const tags = await rl.question( 28 | 'Add the tags you would like? (separated by comma) ' 29 | ) 30 | 31 | rl.close() 32 | 33 | const slug = slugify(title) 34 | 35 | const dir = path.resolve('.') 36 | const filename = `${slug}.md` 37 | const output = path.join(dir, 'content/articles', filename) 38 | 39 | console.log('\n✍️ Drafting new doc...\n') 40 | 41 | try { 42 | const post = { 43 | ...defaults, 44 | title, 45 | tags, 46 | slug, 47 | } 48 | let frontmatter = '---\n' 49 | 50 | for (const [k, v] of Object.entries(post)) { 51 | frontmatter += `${k}: ${v}\n` 52 | } 53 | 54 | frontmatter += '---\n' 55 | 56 | await fs.writeFile(output, frontmatter, { 57 | encoding: 'utf-8', 58 | }) 59 | console.log('✅ Open: ', output) 60 | } catch (error) { 61 | console.error(error) 62 | } 63 | -------------------------------------------------------------------------------- /src/composables/useContributions.js: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useQuery } from '@urql/vue' 3 | 4 | const pullRequestQuery = ` 5 | query { 6 | user(login: "jeferson-sb") { 7 | pullRequests(first: 100, orderBy: { field: CREATED_AT, direction: DESC }, states: [OPEN, MERGED]) { 8 | edges { 9 | node { 10 | id 11 | number 12 | url 13 | createdAt 14 | isCrossRepository 15 | repository { 16 | id 17 | isPrivate 18 | nameWithOwner 19 | url 20 | description 21 | primaryLanguage { 22 | name 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | ` 31 | 32 | export function useContributions() { 33 | const { data } = useQuery({ 34 | query: pullRequestQuery, 35 | }) 36 | 37 | const lastThreeMonths = () => { 38 | const today = new Date() 39 | today.setMonth(today.getMonth() - 3) 40 | return today 41 | } 42 | 43 | const pullRequests = computed(() => 44 | data.value?.user?.pullRequests?.edges?.filter((pr, index, arr) => { 45 | const isFork = pr.node.isCrossRepository 46 | const isRecent = new Date(pr.node.createdAt) >= lastThreeMonths() 47 | const nonDuplicate = 48 | arr.findIndex((x) => x.node.repository.id === pr.node.repository.id) === 49 | index 50 | return isFork && isRecent && nonDuplicate 51 | }) 52 | ) 53 | 54 | return pullRequests 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ui/ImgLazy.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 51 | 52 | 81 | -------------------------------------------------------------------------------- /src/pages/articles/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | 45 | 53 | -------------------------------------------------------------------------------- /src/components/svg/PullRequestSVG.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/composables/useCoffeeControl.js: -------------------------------------------------------------------------------- 1 | import { onMounted, watch } from 'vue' 2 | import { event } from 'vue-gtag' 3 | 4 | import { isProd } from '@lib/modes' 5 | import debounce from '@lib/handling/debounce' 6 | import { getVisitorId } from '@lib/visitor/getVisitorId' 7 | import { useCoffeeStore } from '@/composables/useCoffeeStore' 8 | 9 | export function useCoffeeControl({ articleId, articleUrl }) { 10 | if (!articleId && !articleUrl) { 11 | throw new Error('Article url and id are required') 12 | } 13 | 14 | const { coffee, addCoffeePoints } = useCoffeeStore() 15 | const points = ref(0) 16 | const visitor = ref('') 17 | 18 | const allCupsCoffee = computed(() => { 19 | return coffee.value.reduce((total, coffeeCup) => { 20 | const articlePoints = coffeeCup.articles[articleId]?.points 21 | return articlePoints ? articlePoints + total : total 22 | }, points.value) 23 | }) 24 | 25 | onMounted(async () => { 26 | try { 27 | const visitorId = await getVisitorId() 28 | const visitorDoc = coffee.value.find((coff) => coff.id === visitorId) 29 | const visitorGivenPoints = visitorDoc?.articles[articleId]?.points 30 | 31 | visitor.value = visitorId 32 | points.value = visitorGivenPoints || 0 33 | } catch (error) { 34 | console.error(error) 35 | } 36 | }) 37 | 38 | const onPointsWatch = debounce(async () => { 39 | if (isProd) { 40 | await addCoffeePoints({ 41 | articleId, 42 | points: points.value, 43 | visitorId: visitor.value, 44 | }) 45 | event('coffee_click', { article: articleUrl }) 46 | } 47 | }, 400) 48 | 49 | watch(points, onPointsWatch) 50 | 51 | return { 52 | points, 53 | allCupsCoffee, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/base/AppLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 40 | 41 | 75 | -------------------------------------------------------------------------------- /src/components/ui/ThemeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 47 | 48 | 75 | -------------------------------------------------------------------------------- /src/components/demos/AnimatedBox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | 36 | 84 | -------------------------------------------------------------------------------- /src/components/ui/RippleButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 81 | -------------------------------------------------------------------------------- /src/components/forms/BaseRange.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 72 | -------------------------------------------------------------------------------- /src/components/article/ArticleControls.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 82 | -------------------------------------------------------------------------------- /src/components/svg/GridsomeSVG.vue: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/components/layout/Footer.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio", 3 | "version": "2.1.0", 4 | "private": true, 5 | "license": "Apache License 2.0", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite-ssg build", 9 | "dev": "vite", 10 | "preview": "vite preview", 11 | "optimize": "vite optimize", 12 | "lint:js": "eslint 'src/**/*.{js,vue}'", 13 | "lint:css": "stylelint 'src/**/*.{css,vue}'", 14 | "format": "prettier 'src/**/*.js' -w", 15 | "deps:up": "taze major -I", 16 | "start:draft": "node scripts/create-draft.js", 17 | "bundlesize": "npx vite-bundle-visualizer" 18 | }, 19 | "dependencies": { 20 | "@fingerprintjs/fingerprintjs": "^5.0.1", 21 | "@unhead/vue": "^2.0.19", 22 | "@urql/vue": "^1.4.3", 23 | "firebase": "^11.10.0", 24 | "graphql": "^16.12.0", 25 | "lozad": "^1.16.0", 26 | "vue": "^3.5.25", 27 | "vue-gtag": "^3.6.3", 28 | "vue-router": "^4.6.3", 29 | "vuefire": "^3.2.2" 30 | }, 31 | "devDependencies": { 32 | "@csstools/postcss-global-data": "^3.1.0", 33 | "@vitejs/plugin-vue": "^6.0.2", 34 | "beasties": "^0.3.5", 35 | "eslint": "^9.39.1", 36 | "eslint-config-prettier": "^10.1.8", 37 | "eslint-plugin-import": "^2.32.0", 38 | "eslint-plugin-prettier": "^5.5.4", 39 | "eslint-plugin-vue": "^10.5.1", 40 | "gray-matter": "^4.0.3", 41 | "markdown-it-anchor": "^9.2.0", 42 | "markdown-it-shiki": "^0.9.0", 43 | "postcss-preset-env": "^10.4.0", 44 | "prettier": "^3.7.3", 45 | "stylelint": "^16.26.1", 46 | "stylelint-config-recommended-vue": "^1.6.1", 47 | "stylelint-config-standard": "^38.0.0", 48 | "taze": "^19.9.2", 49 | "unplugin-auto-import": "^19.3.0", 50 | "unplugin-vue-components": "^28.8.0", 51 | "unplugin-vue-markdown": "^29.2.0", 52 | "vite": "^7.2.6", 53 | "vite-plugin-pages": "^0.33.1", 54 | "vite-plugin-pwa": "^1.1.0", 55 | "vite-plugin-vue-devtools": "^7.7.7", 56 | "vite-plugin-vue-layouts": "^0.11.0", 57 | "vite-ssg": "^28.2.2", 58 | "vite-ssg-sitemap": "^0.9.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/demos/ContainerCard.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 86 | -------------------------------------------------------------------------------- /src/components/hero/HeroAvatar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 92 | -------------------------------------------------------------------------------- /src/components/demos/BreadcrumbStyle.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 98 | -------------------------------------------------------------------------------- /src/assets/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable declaration-no-important */ 2 | @layer reset { 3 | 4 | /* Box sizing rules */ 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: border-box; 9 | } 10 | 11 | /* Remove default padding */ 12 | ul, 13 | ol { 14 | padding: 0; 15 | } 16 | 17 | /* Remove default margin */ 18 | body, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | p, 24 | ul[class], 25 | ol[class], 26 | li, 27 | figure, 28 | figcaption, 29 | blockquote, 30 | dl, 31 | dd { 32 | margin: 0; 33 | } 34 | 35 | html, 36 | body { 37 | min-height: 100%; 38 | } 39 | 40 | #app { 41 | min-height: 100dvh; 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: space-between; 45 | } 46 | 47 | html:focus-within { 48 | scroll-behavior: smooth; 49 | } 50 | 51 | /* A elements that don't have a class get default styles */ 52 | a:not([class]) { 53 | text-decoration-skip-ink: auto; 54 | } 55 | 56 | /* Make medias easier to work with */ 57 | img, 58 | picture, 59 | video, 60 | canvas, 61 | svg, 62 | iframe, 63 | object { 64 | max-width: 100%; 65 | display: block; 66 | } 67 | 68 | /* Natural flow and rhythm in articles by default */ 69 | article>*+* { 70 | margin-block-start: 1em; 71 | } 72 | 73 | /* Avoid text overflows */ 74 | p, 75 | h1, 76 | h2, 77 | h3, 78 | h4, 79 | h5, 80 | h6 { 81 | overflow-wrap: break-word; 82 | } 83 | 84 | /* Inherit fonts for inputs and buttons */ 85 | input, 86 | button, 87 | textarea, 88 | select { 89 | font: inherit; 90 | } 91 | 92 | button { 93 | border: 0; 94 | cursor: pointer; 95 | } 96 | } 97 | 98 | /* Remove all animations and transitions for people that prefer not to see them */ 99 | @media (prefers-reduced-motion: reduce) { 100 | * { 101 | animation-duration: 0.01ms !important; 102 | animation-iteration-count: 1 !important; 103 | transition-duration: 0.01ms !important; 104 | scroll-behavior: auto; 105 | } 106 | } 107 | 108 | /* Reduce transparency to all applicable elements */ 109 | @media screen and (prefers-reduced-transparency: reduce) { 110 | * { 111 | backdrop-filter: unset; 112 | } 113 | } -------------------------------------------------------------------------------- /src/components/demos/ResponsiveProfileCard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 107 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/github/GithubRepoCard.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 44 | 45 | 106 | -------------------------------------------------------------------------------- /src/components/demos/ContainerUnits.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 72 | 73 | 106 | -------------------------------------------------------------------------------- /src/components/projects/ProjectCard.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 54 | 55 | 117 | -------------------------------------------------------------------------------- /src/components/demos/SmartGrid.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 57 | 58 | 81 | -------------------------------------------------------------------------------- /src/assets/styles/markdown.css: -------------------------------------------------------------------------------- 1 | @layer markdown { 2 | :root { 3 | --shiki-color-text: hsl(0deg 0% 83%); 4 | --shiki-color-background: hsl(235deg 19% 13%); 5 | --shiki-token-constant: hsl(145deg 46% 51%); 6 | --shiki-token-string: hsl(40deg 67% 51%); 7 | --shiki-token-comment: hsl(229deg 23% 44%); 8 | --shiki-token-keyword: hsl(259deg 59% 59%); 9 | --shiki-token-parameter: hsl(0deg 100% 33%); 10 | --shiki-token-function: hsl(266deg 59% 70%); 11 | --shiki-token-string-expression: hsl(190deg 91% 48%); 12 | --shiki-token-punctuation: hsl(0deg 0% 83%); 13 | --shiki-token-link: hsl(0deg 100% 47%); 14 | --shiki-highlited-line: hsl(258deg 50% 52% / 40%); 15 | } 16 | 17 | .shiki { 18 | position: relative; 19 | border-radius: var(--radius-default); 20 | padding: 0.875rem; 21 | overflow-x: auto; 22 | 23 | *::selection { 24 | background: hsl(0deg 0% 0% / 30%); 25 | } 26 | 27 | & code { 28 | counter-reset: step; 29 | counter-increment: step 0; 30 | } 31 | 32 | & span { 33 | font-family: var(--font-mono); 34 | } 35 | 36 | & .line:empty { 37 | display: none; 38 | } 39 | 40 | & .line.highlighted { 41 | display: inline-block; 42 | background: var(--shiki-highlited-line); 43 | width: 100%; 44 | border-radius: var(--radius-default); 45 | } 46 | 47 | & code:has(.line:nth-child(3n)) .line::before { 48 | display: inline-block; 49 | width: 10px; 50 | content: counter(step); 51 | counter-increment: step; 52 | color: hsl(0deg 0% 60%); 53 | margin-inline-end: 1.5rem; 54 | } 55 | 56 | &::-webkit-scrollbar { 57 | width: 1em; 58 | } 59 | 60 | &::-webkit-scrollbar-track { 61 | background-color: var(--scrollbar-bg); 62 | box-shadow: inset 0 0 6px rgb(0 0 0 / 50%); 63 | border-radius: 0; 64 | } 65 | 66 | &::-webkit-scrollbar-thumb { 67 | border-radius: 10px; 68 | background-color: var(--scrollbar-thumb-color); 69 | border: 5px solid var(--scrollbar-border); 70 | } 71 | 72 | & .copy-to-clipboard { 73 | position: absolute; 74 | top: 20px; 75 | right: 20px; 76 | display: inline-flex; 77 | align-items: center; 78 | justify-content: center; 79 | background: transparent; 80 | opacity: 0.4; 81 | border: 1px solid var(--color-silver); 82 | border-radius: var(--radius-default); 83 | padding: 0.25rem; 84 | transition: opacity 400ms ease-out, scale 600ms ease-in; 85 | } 86 | 87 | & .copy-to-clipboard:active { 88 | scale: 0.4; 89 | } 90 | 91 | & .copy-to-clipboard:hover { 92 | opacity: 0.8; 93 | } 94 | 95 | & .copy-to-clipboard svg { 96 | width: 24px; 97 | height: 24px; 98 | } 99 | 100 | & code:not(:has(.line:nth-child(3n)))+.copy-to-clipboard { 101 | top: 10px; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/article/GalaxyBackground.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/layout/MobileNavbar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | 33 | 138 | -------------------------------------------------------------------------------- /src/pages/now.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: /now 3 | meta: 4 | - name: description 5 | content: What it is happening now? 6 | - name: og:title 7 | content: /now 8 | - name: twitter:title 9 | content: /now 10 | --- 11 | 12 |
13 | 14 | 15 | 16 | # Current Status 17 | 18 | I'm currently reading: Software Architecture: The Hard Parts by Neal Ford & cia. 19 | 20 | If you have suggestions or wish I write about any other topic, [feel free to submit here](https://forms.gle/ftUPgfqW7ghzQgTY9). 21 | 22 | I'm not really active in social media, but if you want to get in touch: 23 | 24 | 🌍 Socials: 25 | 26 | - Bluesky: [@jeferson-brito.bsky.social](https://bsky.app/profile/jeferson-brito.bsky.social) 27 | - Mastodon: [@jeferson_brito](https://mastodon.social/@jeferson_brito) 28 | 29 | 📺 Watching: Wind Breaker, DanDaDan, Apothecary diaries 30 | 31 | 🎮 Currently playing: Spiderman Remastered, Persona Phantom X. 32 | 33 |
34 | 35 |
36 | gif of the day 37 |
(from Tenor)
38 |
39 | 40 | 136 | -------------------------------------------------------------------------------- /src/components/demos/CustomThemes.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 72 | 73 | 123 | -------------------------------------------------------------------------------- /src/components/svg/SpotLightSVG.vue: -------------------------------------------------------------------------------- 1 | 66 | -------------------------------------------------------------------------------- /src/components/demos/ColorMix.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 69 | 70 | 124 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const EffectScope: typeof import('vue')['EffectScope'] 5 | const computed: typeof import('vue')['computed'] 6 | const createApp: typeof import('vue')['createApp'] 7 | const customRef: typeof import('vue')['customRef'] 8 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 9 | const defineComponent: typeof import('vue')['defineComponent'] 10 | const effectScope: typeof import('vue')['effectScope'] 11 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 12 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 13 | const h: typeof import('vue')['h'] 14 | const inject: typeof import('vue')['inject'] 15 | const isProxy: typeof import('vue')['isProxy'] 16 | const isReactive: typeof import('vue')['isReactive'] 17 | const isReadonly: typeof import('vue')['isReadonly'] 18 | const isRef: typeof import('vue')['isRef'] 19 | const markRaw: typeof import('vue')['markRaw'] 20 | const nextTick: typeof import('vue')['nextTick'] 21 | const onActivated: typeof import('vue')['onActivated'] 22 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 23 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 24 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 25 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 26 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 27 | const onDeactivated: typeof import('vue')['onDeactivated'] 28 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 29 | const onMounted: typeof import('vue')['onMounted'] 30 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 31 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 32 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 33 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 34 | const onUnmounted: typeof import('vue')['onUnmounted'] 35 | const onUpdated: typeof import('vue')['onUpdated'] 36 | const provide: typeof import('vue')['provide'] 37 | const reactive: typeof import('vue')['reactive'] 38 | const readonly: typeof import('vue')['readonly'] 39 | const ref: typeof import('vue')['ref'] 40 | const resolveComponent: typeof import('vue')['resolveComponent'] 41 | const resolveDirective: typeof import('vue')['resolveDirective'] 42 | const shallowReactive: typeof import('vue')['shallowReactive'] 43 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 44 | const shallowRef: typeof import('vue')['shallowRef'] 45 | const toRaw: typeof import('vue')['toRaw'] 46 | const toRef: typeof import('vue')['toRef'] 47 | const toRefs: typeof import('vue')['toRefs'] 48 | const triggerRef: typeof import('vue')['triggerRef'] 49 | const unref: typeof import('vue')['unref'] 50 | const useAttrs: typeof import('vue')['useAttrs'] 51 | const useCssModule: typeof import('vue')['useCssModule'] 52 | const useCssVars: typeof import('vue')['useCssVars'] 53 | const useHead: typeof import('@unhead/vue')['useHead'] 54 | const useLink: typeof import('vue-router')['useLink'] 55 | const useRoute: typeof import('vue-router')['useRoute'] 56 | const useRouter: typeof import('vue-router')['useRouter'] 57 | const useSlots: typeof import('vue')['useSlots'] 58 | const watch: typeof import('vue')['watch'] 59 | const watchEffect: typeof import('vue')['watchEffect'] 60 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 61 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 62 | } 63 | -------------------------------------------------------------------------------- /src/components/hero/HeroSection.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 135 | -------------------------------------------------------------------------------- /src/components/svg/LampOnSVG.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 149 | -------------------------------------------------------------------------------- /src/components/svg/NetlifyLogoSVG.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import fs from 'node:fs' 3 | import { defineConfig } from 'vite' 4 | import Vue from '@vitejs/plugin-vue' 5 | import Pages from 'vite-plugin-pages' 6 | import Layouts from 'vite-plugin-vue-layouts' 7 | import DevTools from 'vite-plugin-vue-devtools' 8 | import Components from 'unplugin-vue-components/vite' 9 | import AutoImport from 'unplugin-auto-import/vite' 10 | import Markdown from 'unplugin-vue-markdown/vite' 11 | import { VitePWA } from 'vite-plugin-pwa' 12 | import Shiki from 'markdown-it-shiki' 13 | import MarkdownItAnchor from 'markdown-it-anchor' 14 | import matter from 'gray-matter' 15 | import generateSitemap from 'vite-ssg-sitemap' 16 | 17 | import { copyPlugin } from './lib/md/copy-code' 18 | 19 | export default defineConfig({ 20 | resolve: { 21 | alias: { 22 | '@/': `${path.resolve(__dirname, 'src')}/`, 23 | '@content': `${path.resolve(__dirname, 'content')}/`, 24 | '@lib': `${path.resolve(__dirname, 'lib')}` 25 | }, 26 | }, 27 | plugins: [ 28 | Vue({ 29 | include: [/\.vue$/, /\.md$/], 30 | }), 31 | 32 | Pages({ 33 | extensions: ['vue', 'md'], 34 | dirs: ['src/pages', { dir: 'content/articles', baseRoute: 'articles' }], 35 | exclude: ['**/draft/*'], 36 | extendRoute(route, _parent) { 37 | const isArticlePost = 38 | route.path.includes('/articles') && route.path !== '/articles' 39 | const componentPath = path.resolve(__dirname, route.component.slice(1)) 40 | 41 | if (isArticlePost) { 42 | const md = fs.readFileSync(componentPath, 'utf-8') 43 | const { data: frontmatter } = matter(md) 44 | 45 | return { 46 | ...route, 47 | meta: { layout: 'article', frontmatter }, 48 | } 49 | } 50 | 51 | return route 52 | }, 53 | }), 54 | 55 | Layouts(), 56 | 57 | AutoImport({ 58 | imports: ['vue', 'vue-router', { '@unhead/vue': ['useHead'] }], 59 | dirs: [ 60 | 'src/composables', 61 | 'src/components' 62 | ] 63 | }), 64 | 65 | Components({ 66 | extensions: ['vue', 'md'], 67 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 68 | }), 69 | 70 | Markdown({ 71 | wrapperClasses: 'prose', 72 | headEnabled: true, 73 | markdownItSetup(md) { 74 | md.use(copyPlugin); 75 | md.use(Shiki, { 76 | theme: 'css-variables', 77 | highlightLines: true, 78 | }) 79 | md.use(MarkdownItAnchor, { 80 | permalink: MarkdownItAnchor.permalink.linkInsideHeader({ 81 | symbol: ` 82 | Jump to heading 83 | 84 | 85 | 86 | `, 87 | placement: 'before', 88 | }), 89 | }) 90 | }, 91 | }), 92 | 93 | VitePWA({ 94 | registerType: 'autoUpdate', 95 | workbox: { 96 | globPatterns: ['**/*{js,css,html,txt,ico,png,svg}'], 97 | cleanupOutdatedCaches: true, 98 | }, 99 | }), 100 | 101 | DevTools() 102 | ], 103 | ssgOptions: { 104 | script: 'async', 105 | formatting: 'minify', 106 | beastiesOptions: { 107 | reduceInlineStyles: false, 108 | }, 109 | onFinished() { 110 | generateSitemap() 111 | }, 112 | }, 113 | optimizeDeps: { 114 | include: ['vue', 'vue-router'], 115 | }, 116 | server: { 117 | port: 3000, 118 | }, 119 | preview: { 120 | port: 8080, 121 | }, 122 | }) 123 | -------------------------------------------------------------------------------- /src/components/demos/AnimatedSVG.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/ui/Button.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 67 | 68 | 184 | -------------------------------------------------------------------------------- /content/articles/how-to-keep-up.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mhri2af6i 3 | title: How to keep up with things 4 | published_at: 2024-05-17T22:55:34.127Z 5 | tags: opinion 6 | excerpt: I have a feeling we are on a constant flux of ever-evolving modern world where saying updated is integral to our personal growth... 7 | slug: how-to-keep-up 8 | crosspostedOn: '' 9 | crosspostLink: '' 10 | og_image: https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/og%2Fhow-to-keep-up.webp?alt=media&token=3236f604-7b3e-41df-b062-c977a1fb633f 11 | --- 12 | 13 | I have a feeling we are in constant flux in the ever-evolving modern world where staying updated is integral to our personal growth. You want to watch every basketball match of your favorite team this season and catch up with new episodes of One Piece but you've stopped on episode 45, watch every talk, follow every new trend, learn every new possible feature, and have a normal life? 14 | 15 | How can you possibly keep up with all of this? 16 | Short answer: **you can't**. 17 | 18 | You could say you were able to do this before, so why suddenly you can't do it anymore? Well, things change all the time, and they change differently. 19 | 20 | Especially in the **programming realm**, take web development for instance, it seems things are being delivered at an incredible pace right now, and this is for good. Just pause and imagine going back 3 or 4 years ago, how your JavaScript or CSS (especially) code was compared to what it can be today (with no extra tools!), it's crazy how many features we have now. 21 | 22 | So, what can you do to still be up to date without being exhausted after being bombarded with information? 23 | 24 | Well, I start by sharing some thoughts I had when I had this feeling. 25 | 26 | ## Don’t get trapped in the FOMO loop 27 | 28 | It is natural to feel excited when new things come out, Gemini, GPT-4o, Blue Origin, and so on. As I started learning programming I wanted to try every new thing that came out that I found interesting which led me from countless hours experimenting to, most of the time, not applying them in real projects. It can feel very frustrating at times. 29 | 30 | These are things that can keep you in a fear of missing out (FOMO) loop to having consume much more content than you needed to not stay "behind" or to not feel like missing something critical, when, most of the time, you're weren't. Remember, technology isn't deterministic, things can still change. 31 | 32 | You see, I needed to let things mature first and move at my own pace while not being completely disconnected from what is happening around the world. 33 | 34 | This led me to prioritizing and filtering what kind of content I consumed frequently. 35 | 36 | ## Filter noise 37 | 38 | What do you mean _noise_ ? You might ask. 39 | 40 | In the context of this post, you can refer to noise as **information that is either redundant or not entirely relevant to you at this moment**. This includes any information that does not align with your interests, goals, or needs at the current time. 41 | 42 | To manage this "noise cancellation", I needed to essentially curate the sources of information I was typically used to consuming on a daily basis. Although this required me some amount of good discipline, once I started to do that, I could see myself having more focus on doing regular tasks and having more spare time in the end. 43 | 44 | In the same way, each person can have a stronger learning by reading instead of watching, each one can have valid sources of information they can rely on. For me, this meant focusing on a more narrow set of authors that I really like and seeing what they were doing on their personal blog posts or podcasts, instead of seeing on social media. 45 | 46 | ## Find your own pace 47 | 48 | Everybody learns at different paces and sometimes in unique ways. Don't just give up what you're doing now to involved in a new project to use the shiny new tool XYZ. Understand the moment you're leading to. 49 | 50 | What helped me in my journey was to always have a clear **main focus** first then a list of things that I could potentially experiment with to solve challenges when needed to. This could be an algorithm, a data structure, a library or a framework. 51 | 52 | Last but not least, make learning a continuous habit. If you feel free enough to experiment with new things you're interested at the moment, then go ahead and try! Otherwise, write them down somewhere to explore later. 53 | 54 | Take breaks, follow your goals, and set your eyes on the future! 55 | -------------------------------------------------------------------------------- /src/components/demos/GeneratePallete.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 87 | 88 | 142 | -------------------------------------------------------------------------------- /src/components/ui/CoffeeButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 53 | 54 | 192 | -------------------------------------------------------------------------------- /content/projects/projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "aircnc": { 3 | "order": 1, 4 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Faircnc.png?alt=media&token=0e827724-c281-4196-b801-48c0194cdbf0", 5 | "title": "Aircnc", 6 | "tags": "Fullstack", 7 | "demo": "https://aircnc-web.herokuapp.com/", 8 | "github": "https://github.com/jeferson-sb/aircnc" 9 | }, 10 | "chatme": { 11 | "order": 2, 12 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fchatme.png?alt=media&token=f1a2d950-6c59-4fe6-a9ea-e9d11b8ee255", 13 | "title": "ChatMe", 14 | "tags": "Node.js, Vue.js, Socket.io", 15 | "demo": "https://whispering-reaches-62773.herokuapp.com/", 16 | "github": "https://github.com/jeferson-sb/node-chat-app", 17 | "pinned": true, 18 | "description": "Lightweight chat app built with Vue 2, Express and Socket.io" 19 | }, 20 | "whatconf": { 21 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fwhatconf.png?alt=media&token=a34f3155-b326-4944-a767-82fcc108092e", 22 | "title": "WhatConf", 23 | "tags": "Next.js, tRPC, Prisma", 24 | "demo": "https://whatconf.vercel.app/", 25 | "github": "https://github.com/jeferson-sb/whatconf", 26 | "pinned": true, 27 | "description": "Tech conferences feed that users can check its date and details and subscribe to receive web notifications!" 28 | }, 29 | "dogAI": { 30 | "order": 1, 31 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2FdogAI.jpg?alt=media&token=8a340fd4-749b-41c8-8200-f51db8df27cf", 32 | "title": "Dog AI", 33 | "tags": "Front-end, React, Tensorflow", 34 | "demo": "https://dogai.netlify.com/", 35 | "github": "https://github.com/jeferson-sb/dogAI", 36 | "pinned": true, 37 | "description": "Dog breed image recognition with Teachable Machine & Tensorflow.js" 38 | }, 39 | "flashmemo": { 40 | "order": 3, 41 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fflashmemo.jpg?alt=media&token=355cda40-b248-44fb-92b3-eae0e0bf3a0f", 42 | "title": "Flashmemo", 43 | "tags": "Ruby on Rails", 44 | "github": "https://github.com/jeferson-sb/flashmemo", 45 | "pinned": true, 46 | "description": "REST API with rewards system designed to help users practice their knowledge by answering exams." 47 | }, 48 | "devradar": { 49 | "order": 4, 50 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fdevradar.jpg?alt=media&token=1f23fb7e-23e2-4270-a057-d724b3b880eb", 51 | "title": "DevRadar", 52 | "tags": "Full Stack", 53 | "demo": "https://devradar-api.appspot.com/", 54 | "github": "https://github.com/jeferson-sb/devradar" 55 | }, 56 | "gobarber": { 57 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fgobarber.jpg?alt=media&token=9a733a14-f498-465a-96dd-e034f773513a", 58 | "title": "GoBarber", 59 | "tags": "Node.js, React, TypeScript", 60 | "github": "https://github.com/jeferson-sb/gobarber-web", 61 | "description": "React app to manage haircuts appointments" 62 | }, 63 | "ecoleta": { 64 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fecoleta.png?alt=media&token=43931213-70df-4a66-9cf4-d6bb6fe50889", 65 | "title": "Ecoleta", 66 | "tags": "React, React Native, Node.js, TypeScript", 67 | "demo": "https://ecoleta-nlw.netlify.app/", 68 | "github": "https://github.com/jeferson-sb/ecoleta", 69 | "description": "Marketplace de coleta de resíduos" 70 | }, 71 | "tindev": { 72 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Ftindev.png?alt=media&token=f59beae0-3f38-45dc-90ef-8d1ec87b0f43", 73 | "title": "Tindev", 74 | "tags": "React, React Native, Node.js, TypeScript", 75 | "demo": "https://tindev-app.herokuapp.com", 76 | "github": "https://github.com/jeferson-sb/tindev" 77 | }, 78 | "bethehero": { 79 | "order": 4, 80 | "thumbnail": "https://firebasestorage.googleapis.com/v0/b/portfolio-d3c7c.appspot.com/o/posts_media%2Fbethehero.png?alt=media&token=5af9bc3e-0e5f-4758-bc24-8775d4fdd10a", 81 | "title": "Be the Hero", 82 | "tags": "Node.js, React, React Native", 83 | "demo": "https://be-thehero.netlify.com/", 84 | "github": "https://github.com/jeferson-sb/be-the-hero", 85 | "pinned": false, 86 | "description": "Donate System for NGOs campaigns" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/pages/about-me.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 67 | 68 | 69 | meta: 70 | layout: default 71 | 72 | 73 | 195 | -------------------------------------------------------------------------------- /src/components/demos/Resizable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AnimatedBlocks: typeof import('./src/components/demos/AnimatedBlocks.vue')['default'] 11 | AnimatedBook: typeof import('./src/components/demos/AnimatedBook.vue')['default'] 12 | AnimatedBox: typeof import('./src/components/demos/AnimatedBox.vue')['default'] 13 | AnimatedButton: typeof import('./src/components/demos/AnimatedButton.vue')['default'] 14 | AnimatedSVG: typeof import('./src/components/demos/AnimatedSVG.vue')['default'] 15 | AnimatedSVGs: typeof import('./src/components/demos/AnimatedSVGs.vue')['default'] 16 | AppLink: typeof import('./src/components/base/AppLink.vue')['default'] 17 | ArticleControls: typeof import('./src/components/article/ArticleControls.vue')['default'] 18 | ArticleHeading: typeof import('./src/components/article/ArticleHeading.vue')['default'] 19 | ArticlesHero: typeof import('./src/components/article/ArticlesHero.vue')['default'] 20 | BaseInput: typeof import('./src/components/forms/BaseInput.vue')['default'] 21 | BaseRange: typeof import('./src/components/forms/BaseRange.vue')['default'] 22 | BaseSelect: typeof import('./src/components/forms/BaseSelect.vue')['default'] 23 | BinaryTextSVG: typeof import('./src/components/svg/BinaryTextSVG.vue')['default'] 24 | Button: typeof import('./src/components/ui/Button.vue')['default'] 25 | ButtonGroup: typeof import('./src/components/ui/ButtonGroup.vue')['default'] 26 | CircleFillSVG: typeof import('./src/components/svg/CircleFillSVG.vue')['default'] 27 | Clock: typeof import('./src/components/ui/Clock.vue')['default'] 28 | CoffeeButton: typeof import('./src/components/ui/CoffeeButton.vue')['default'] 29 | CoffeeSVG: typeof import('./src/components/svg/CoffeeSVG.vue')['default'] 30 | ColorBox: typeof import('./src/components/demos/ColorBox.vue')['default'] 31 | ColorMix: typeof import('./src/components/demos/ColorMix.vue')['default'] 32 | CustomList: typeof import('./src/components/article/CustomList.vue')['default'] 33 | CustomThemes: typeof import('./src/components/demos/CustomThemes.vue')['default'] 34 | Footer: typeof import('./src/components/layout/Footer.vue')['default'] 35 | GalaxyBackground: typeof import('./src/components/article/GalaxyBackground.vue')['default'] 36 | GeneratePallete: typeof import('./src/components/demos/GeneratePallete.vue')['default'] 37 | GithubCardEmpty: typeof import('./src/components/github/GithubCardEmpty.vue')['default'] 38 | GithubProjects: typeof import('./src/components/github/GithubProjects.vue')['default'] 39 | GithubRepoCard: typeof import('./src/components/github/GithubRepoCard.vue')['default'] 40 | GridsomeSVG: typeof import('./src/components/svg/GridsomeSVG.vue')['default'] 41 | HamburgerMenu: typeof import('./src/components/ui/HamburgerMenu.vue')['default'] 42 | HeroAvatar: typeof import('./src/components/hero/HeroAvatar.vue')['default'] 43 | HeroSection: typeof import('./src/components/hero/HeroSection.vue')['default'] 44 | ImgLazy: typeof import('./src/components/ui/ImgLazy.vue')['default'] 45 | InteractiveWrapper: typeof import('./src/components/ui/InteractiveWrapper.vue')['default'] 46 | LampOffSVG: typeof import('./src/components/svg/LampOffSVG.vue')['default'] 47 | LampOnSVG: typeof import('./src/components/svg/LampOnSVG.vue')['default'] 48 | MobileNavbar: typeof import('./src/components/layout/MobileNavbar.vue')['default'] 49 | Navbar: typeof import('./src/components/layout/Navbar.vue')['default'] 50 | NetlifyLogoSVG: typeof import('./src/components/svg/NetlifyLogoSVG.vue')['default'] 51 | OctocatSVG: typeof import('./src/components/svg/OctocatSVG.vue')['default'] 52 | PagesSVG: typeof import('./src/components/svg/PagesSVG.vue')['default'] 53 | PostPreview: typeof import('./src/components/posts/PostPreview.vue')['default'] 54 | PostsList: typeof import('./src/components/posts/PostsList.vue')['default'] 55 | ProjectCard: typeof import('./src/components/projects/ProjectCard.vue')['default'] 56 | ProjectsGrid: typeof import('./src/components/projects/ProjectsGrid.vue')['default'] 57 | PullRequestSVG: typeof import('./src/components/svg/PullRequestSVG.vue')['default'] 58 | RippleButton: typeof import('./src/components/ui/RippleButton.vue')['default'] 59 | RouterLink: typeof import('vue-router')['RouterLink'] 60 | RouterView: typeof import('vue-router')['RouterView'] 61 | ScrollIndicator: typeof import('./src/components/ui/ScrollIndicator.vue')['default'] 62 | ShareButton: typeof import('./src/components/ui/ShareButton.vue')['default'] 63 | SkipLink: typeof import('./src/components/ui/SkipLink.vue')['default'] 64 | SmartGrid: typeof import('./src/components/demos/SmartGrid.vue')['default'] 65 | SocialMediaIcons: typeof import('./src/components/social-media/SocialMediaIcons.vue')['default'] 66 | SpotLightSVG: typeof import('./src/components/svg/SpotLightSVG.vue')['default'] 67 | Tag: typeof import('./src/components/ui/Tag.vue')['default'] 68 | TagGroup: typeof import('./src/components/ui/TagGroup.vue')['default'] 69 | ThemeSwitcher: typeof import('./src/components/ui/ThemeSwitcher.vue')['default'] 70 | VueSVG: typeof import('./src/components/svg/VueSVG.vue')['default'] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/posts/PostPreview.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 82 | 83 | 217 | -------------------------------------------------------------------------------- /src/assets/styles/global.css: -------------------------------------------------------------------------------- 1 | /* Themes and defaults */ 2 | 3 | @custom-media --vw-sm (max-width: 26.56em); 4 | @custom-media --vw-md (max-width: 48em); 5 | @custom-media --vw-lg (max-width: 64em); 6 | @custom-media --vw-xl (max-width: 90em); 7 | 8 | @layer global { 9 | :root { 10 | 11 | /* Colors */ 12 | --color-primary: oklch(0.7282 0.1222 209.58); 13 | --color-accent: oklch(70.662% 0.15099 54.296); 14 | --color-silver: oklch(56.83% 0.04657 270.021); 15 | --color-silver-600: oklch(66.362% 0.05373 271.257); 16 | --color-default-black: oklch(22.559% 0.00659 258.421); 17 | --color-default-white: oklch(88.531% 0.0001 271.152); 18 | --color-gray-800: oklch(26.721% 0.00866 255.642); 19 | --color-gray-700: oklch(35.827% 0.00663 248.157); 20 | --color-gray-600: oklch(45.747% 0.0172 254.764); 21 | --color-gray-500: oklch(54.52% 0.02174 255.656); 22 | --color-gray-400: oklch(63.336% 0.02095 255.643); 23 | --color-gray-300: oklch(71.914% 0.01538 254.731); 24 | --color-gray-200: oklch(80.047% 0.01089 261.871); 25 | --color-gray-100: oklch(88.108% 0.00648 255.725); 26 | --text-color-default: var(--color-default-white); 27 | --paragraph-color: oklch(84.522% 0.0001 271.152); 28 | --code-bg-color: oklch(29.252% 0.0157 264.319); 29 | --code-highlight-color: oklch(72.72% 0.12217 210.085 / 0.05); 30 | 31 | /* Typography */ 32 | --text-xsm: 0.75rem; 33 | --text-sm: 0.875rem; 34 | --text-base: 1rem; 35 | --text-lg: 1.125rem; 36 | --text-xl: 1.25rem; 37 | --text-2xl: 2.5rem; 38 | --text-3xl: 3.75rem; 39 | 40 | --fw-normal: 500; 41 | --fw-bold: 700; 42 | 43 | --font-sans: 'Source Sans Pro', -apple-system, 'Segoe UI', 44 | helvetica sans-serif; 45 | --font-title: 'Raleway', sans-serif; 46 | --font-serif: 'Roboto Slab', palatino, georgia, serif; 47 | --font-mono: 'Source Code Pro', 'SF Mono', consolas, menlo, monospace; 48 | 49 | /* Borders */ 50 | --radius-default: 4px; 51 | --radius-full: 9999px; 52 | 53 | --border-width: 1px; 54 | --border-style: solid; 55 | --border-color: oklch(0% 0 0 / 0.1); 56 | --border: var(--border-width) var(--border-style) var(--border-color); 57 | 58 | /* Shadows */ 59 | --elevation-1: 0 1px 3px oklch(0% 0 0 / 0.2); 60 | --elevation-2: 0 4px 4px oklch(0% 0 0 / 0.23); 61 | --elevation-3: 0 4px 10px oklch(0% 0 0 / 0.07); 62 | --elevation-4: 0 10px 19px oklch(0% 0 0 / 0.27); 63 | --elevation-5: 0 20px 25px -5px oklch(0% 0 0 / 0.3); 64 | 65 | /* Z-layers */ 66 | --z-10: 10; 67 | --z-100: 100; 68 | --z-base: 1; 69 | --z-hide: -1; 70 | 71 | /* Others */ 72 | --scrollbar-bg: oklch(26.721% 0.00866 255.642); 73 | --scrollbar-thumb-color: oklch(56.83% 0.04657 270.021); 74 | --scrollbar-border: var(--color-gray-800); 75 | --selection: oklch(71.531% 0.17937 50.596 / 0.5); 76 | } 77 | 78 | body { 79 | --base-font-size: 16px; 80 | 81 | font-family: var(--font-sans); 82 | font-size: var(--base-font-size); 83 | accent-color: var(--color-silver); 84 | background-color: var(--color-default-black); 85 | color: var(--color-default-white); 86 | color-scheme: light dark; 87 | scrollbar-gutter: stable; 88 | text-rendering: optimizelegibility; 89 | color-rendering: optimizespeed; 90 | -webkit-font-smoothing: antialiased; 91 | -moz-osx-font-smoothing: grayscale; 92 | line-height: 1.5; 93 | } 94 | 95 | ::selection { 96 | background: var(--selection); 97 | color: var(--text-color-default); 98 | } 99 | 100 | h1, 101 | h2, 102 | h3, 103 | h4, 104 | h5, 105 | h6 { 106 | text-wrap: balance; 107 | font-family: var(--font-title); 108 | } 109 | 110 | a { 111 | text-decoration: none; 112 | transition: text-decoration-color 400ms ease-out; 113 | color: var(--color-primary); 114 | } 115 | 116 | a:focus-visible { 117 | outline: 2px dashed var(--color-primary); 118 | } 119 | 120 | a:not([href]) { 121 | color: var(--text-color-default); 122 | opacity: 0.8; 123 | cursor: not-allowed; 124 | } 125 | 126 | a:not([class]):hover, 127 | a[class=""]:hover, 128 | abbr { 129 | text-decoration-line: underline; 130 | text-decoration-color: var(--color-accent); 131 | text-decoration-thickness: 2px; 132 | text-decoration-style: solid; 133 | text-underline-offset: 0.3em; 134 | } 135 | } 136 | 137 | @layer theme { 138 | body[data-theme='light'] { 139 | --color-light: oklch(97.352% 0.00553 274.897); 140 | --text-color-default: var(--color-default-black); 141 | --secondary-text-color: var(--color-silver); 142 | --bg-color-primary-lighter: oklch(72.72% 0.12217 210.085 / 0.3); 143 | --bg-color-lighter: oklch(97.614% 0.00011 271.152); 144 | --accent-color-lighter: oklch(95.533% 0.00842 271.332); 145 | --scrollbar-bg: var(--accent-color-lighter); 146 | --scrollbar-thumb-color: var(--color-gray-300); 147 | --scrollbar-border: var(--color-gray-300); 148 | 149 | background-color: var(--color-light); 150 | color: var(--color-default-black); 151 | transition-property: background-color, color; 152 | transition-duration: 500ms; 153 | transition-timing-function: ease-in-out; 154 | 155 | & :is(footer, header) { 156 | border-color: oklch(56.83% 0.04657 270.021 / 0.3); 157 | } 158 | } 159 | 160 | body[data-theme='dark'] { 161 | img { 162 | filter: brightness(0.8); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/components/layout/Navbar.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 63 | 64 | 209 | -------------------------------------------------------------------------------- /src/components/demos/CSSPreview.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | 37 | 60 | -------------------------------------------------------------------------------- /src/components/article/ArticleHeading.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 47 | 48 | 103 | --------------------------------------------------------------------------------