├── postcss.config.cjs ├── public ├── me.webp ├── favicon.ico ├── og-image.png ├── preview.png ├── globe_preview.png └── fonts │ ├── Satoshi-Variable.ttf │ ├── Satoshi-VariableItalic.ttf │ └── CabinetGrotesk-Variable.ttf ├── tsconfig.eslint.json ├── .github └── FUNDING.yml ├── src ├── riveAnimations │ └── rifle.riv ├── components │ ├── Pulse.astro │ ├── Card │ │ ├── Content.astro │ │ └── index.astro │ ├── Button.astro │ ├── MyStack.astro │ ├── Now.astro │ ├── Blog │ │ └── PostRow.astro │ ├── playground │ │ ├── scroll-1 │ │ │ ├── svg-shapes │ │ │ │ ├── Shape4.svelte │ │ │ │ ├── Shape2.svelte │ │ │ │ ├── Shape1.svelte │ │ │ │ └── Shape3.svelte │ │ │ └── scroll-1.svelte │ │ └── rifle-1.tsx │ ├── ContactsCard.astro │ ├── TimeZoneCard.astro │ ├── AboutMe.astro │ ├── Globe.tsx │ ├── Tooltip │ │ └── index.tsx │ └── IntroCard.astro ├── env.d.ts ├── data │ └── blog │ │ ├── post-3.md │ │ ├── post1.md │ │ └── post2.md ├── lib │ ├── remark-reading-time.mjs │ ├── constants.ts │ └── helpers.ts ├── content.config.ts ├── layouts │ ├── Layout.astro │ ├── LayoutBlogPost.astro │ └── BasicLayout.astro └── pages │ ├── rss.xml.js │ ├── travel.astro │ ├── blog │ ├── index.astro │ └── [id].astro │ └── index.astro ├── .vscode ├── extensions.json └── launch.json ├── svelte.config.js ├── tsconfig.json ├── jsx.d.ts ├── .gitignore ├── eslint.config.js ├── astro.config.mjs ├── LICENSE ├── package.json ├── uno.config.ts └── README.md /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // postcss.config.cjs 2 | module.exports = { 3 | plugins: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /public/me.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/me.webp -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["jsx.d.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/og-image.png -------------------------------------------------------------------------------- /public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/preview.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Ladvace 4 | ko_fi: ladvace 5 | -------------------------------------------------------------------------------- /public/globe_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/globe_preview.png -------------------------------------------------------------------------------- /src/riveAnimations/rifle.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/src/riveAnimations/rifle.riv -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /public/fonts/Satoshi-Variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/fonts/Satoshi-Variable.ttf -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@astrojs/svelte'; 2 | 3 | export default { 4 | preprocess: vitePreprocess(), 5 | }; 6 | -------------------------------------------------------------------------------- /public/fonts/Satoshi-VariableItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/fonts/Satoshi-VariableItalic.ttf -------------------------------------------------------------------------------- /public/fonts/CabinetGrotesk-Variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ladvace/astro-bento-portfolio/HEAD/public/fonts/CabinetGrotesk-Variable.ttf -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js" 6 | } 7 | } -------------------------------------------------------------------------------- /jsx.d.ts: -------------------------------------------------------------------------------- 1 | import "astro/astro-jsx"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // type Element = astroHTML.JSX.Element // We want to use this, but it is defined as any. 6 | type Element = HTMLElement; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Pulse.astro: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Card/Content.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title?: string; 4 | body?: string; 5 | } 6 | 7 | const { title, body } = Astro.props; 8 | --- 9 | 10 | <> 11 | {title &&

{title}

} 12 | {body &&

{body}

} 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | .netlify/ 11 | 12 | # logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | interface ImportMetaEnv { 4 | readonly CONTENTFUL_SPACE_ID: string; 5 | readonly CONTENTFUL_DELIVERY_TOKEN: string; 6 | readonly CONTENTFUL_PREVIEW_TOKEN: string; 7 | } 8 | 9 | declare module "*.riv" { 10 | const content: any; 11 | export default content; 12 | } 13 | -------------------------------------------------------------------------------- /src/data/blog/post-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, World" 3 | description: "this is a post example" 4 | pubDate: 2023-01-21 5 | category: "intro" 6 | draft: false 7 | --- 8 | 9 | # Hi there! 10 | 11 | This Markdown file creates a page at `your-domain.com/blog/post1/` 12 | 13 | It probably isn't styled much, but Markdown does support: 14 | 15 | - **bold** and _italics._ 16 | - lists 17 | - [links](https://astro.build) 18 | - and more! 19 | -------------------------------------------------------------------------------- /src/data/blog/post1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, World" 3 | description: "this is a post example" 4 | pubDate: 2023-01-21 5 | category: "intro" 6 | draft: false 7 | --- 8 | 9 | # Hi there! 10 | 11 | This Markdown file creates a page at `your-domain.com/blog/post1/` 12 | 13 | It probably isn't styled much, but Markdown does support: 14 | 15 | - **bold** and _italics._ 16 | - lists 17 | - [links](https://astro.build) 18 | - and more! 19 | -------------------------------------------------------------------------------- /src/data/blog/post2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, World" 3 | description: "this is a post example" 4 | pubDate: 2023-01-21 5 | category: "intro" 6 | draft: false 7 | --- 8 | 9 | # Hi there! 10 | 11 | This Markdown file creates a page at `your-domain.com/blog/post1/` 12 | 13 | It probably isn't styled much, but Markdown does support: 14 | 15 | - **bold** and _italics._ 16 | - lists 17 | - [links](https://astro.build) 18 | - and more! 19 | -------------------------------------------------------------------------------- /src/lib/remark-reading-time.mjs: -------------------------------------------------------------------------------- 1 | import getReadingTime from "reading-time"; 2 | import { toString } from "mdast-util-to-string"; 3 | 4 | export function remarkReadingTime() { 5 | return function (tree, { data }) { 6 | const textOnPage = toString(tree); 7 | const readingTime = getReadingTime(textOnPage); 8 | // readingTime.text will give us minutes read as a friendly string, 9 | // i.e. "3 min read" 10 | data.astro.frontmatter.minutesRead = readingTime.text; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const LINKS = { 2 | github: "https://github.com/Ladvace", 3 | linkedin: "https://www.linkedin.com/in/gianmarco-cavallo/", 4 | medium: "https://ladvace.medium.com/", 5 | discord: "https://discordapp.com/users/163300027618295808", 6 | dribble: "https://dribbble.com/Ladvace_Jace", 7 | email: "contact@gianmarcocavallo.com<", 8 | }; 9 | 10 | export const loaderAnimation = [ 11 | ".loader", 12 | { opacity: [1, 0], pointerEvents: "none" }, 13 | { easing: "ease-out" }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/components/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | rounded?: boolean; 4 | } 5 | 6 | const { rounded } = Astro.props; 7 | --- 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/components/MyStack.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Card from "./Card/index.astro"; 3 | --- 4 | 5 | 6 |

7 | Here's a snapshot of the primary tools and technologies I work with: 8 |

9 |
    10 |
  • Solidjs
  • 11 |
  • JavaScript
  • 12 |
  • Node.js
  • 13 |
  • React.js
  • 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { glob } from "astro/loaders"; 3 | import { rssSchema } from "@astrojs/rss"; 4 | 5 | const blog = defineCollection({ 6 | loader: glob({ pattern: "**/[^_]*.md", base: "./src/data/blog" }), 7 | schema: rssSchema, 8 | 9 | // schema: z.object({ 10 | // title: z.string(), 11 | // description: z.string(), 12 | // pubDate: z.coerce.date(), 13 | // updatedDate: z.coerce.date().optional(), 14 | // }), 15 | }); 16 | 17 | export const collections = { blog }; 18 | -------------------------------------------------------------------------------- /src/components/Now.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Card from "./Card/index.astro"; 3 | import Pulse from "./Pulse.astro"; 4 | --- 5 | 6 | 7 |
8 |
9 |

Now

10 | 11 | what's that ? 12 | 13 |
14 | 15 |
16 |

Currently working fulltime

17 |
18 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BasicLayout from "./BasicLayout.astro"; 3 | 4 | interface Props { 5 | title: string; 6 | description: string; 7 | page?: "travel"; 8 | fullScreen?: string; 9 | } 10 | 11 | const { title, description } = Astro.props; 12 | --- 13 | 14 | 15 |
18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintPluginAstro from "eslint-plugin-astro"; 2 | 3 | export default [ 4 | ...eslintPluginAstro.configs.base, 5 | ...eslintPluginAstro.configs.recommended, 6 | { 7 | files: ["**/*.{js,astro}"], 8 | rules: { 9 | // Basic JS rules 10 | "no-unused-vars": "warn", 11 | "no-undef": "error", 12 | "no-console": "warn", 13 | "no-debugger": "error", 14 | 15 | // Astro-specific rules 16 | // "astro/no-conflict-set-directives": "error", 17 | "astro/no-unused-define-vars-in-style": "error", 18 | "astro/valid-compile": "error", 19 | 20 | // You can add more rules here as needed 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/components/Blog/PostRow.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { formatDate } from "../../lib/helpers"; 3 | 4 | interface Props { 5 | title: string; 6 | date: Date; 7 | id: string; 8 | } 9 | 10 | const { title, date, id } = Astro.props; 11 | --- 12 | 13 |
  • 16 | 20 |

    21 | {title} 22 |

    23 | 29 |
    30 |
  • 31 | -------------------------------------------------------------------------------- /src/pages/rss.xml.js: -------------------------------------------------------------------------------- 1 | import rss from "@astrojs/rss"; 2 | import { getCollection } from "astro:content"; 3 | import sanitizeHtml from 'sanitize-html'; 4 | import MarkdownIt from 'markdown-it'; 5 | const parser = new MarkdownIt(); 6 | 7 | export async function GET(context) { 8 | const blog = await getCollection("blog"); 9 | return rss({ 10 | title: "Gianmarco Cavallo’s Blog", 11 | description: "my blog", 12 | site: context.site, 13 | items: blog.map((post) => ({ 14 | title: post.data.title, 15 | pubDate: post.data.pubDate, 16 | description: post.data.description, 17 | content: sanitizeHtml(parser.render(post.body)), 18 | // Compute RSS link from post `slug` 19 | // This example assumes all posts are rendered as `/blog/[slug]` routes 20 | link: `/blog/${post.slug}/`, 21 | })), 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/travel.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Globe from "../components/Globe"; 3 | import BasicLayout from "../layouts/BasicLayout.astro"; 4 | --- 5 | 6 | 19 | 20 | 25 | Back 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/playground/scroll-1/svg-shapes/Shape4.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import sitemap from "@astrojs/sitemap"; 3 | import netlify from "@astrojs/netlify"; 4 | import robotsTxt from "astro-robots-txt"; 5 | import UnoCSS from "@unocss/astro"; 6 | import icon from "astro-icon"; 7 | 8 | import solidJs from "@astrojs/solid-js"; 9 | import { remarkReadingTime } from "./src/lib/remark-reading-time.mjs"; 10 | 11 | import svelte from "@astrojs/svelte"; 12 | 13 | // https://astro.build/config 14 | export default defineConfig({ 15 | site: "https://gianmarcocavallo.com/", 16 | integrations: [ 17 | sitemap(), 18 | robotsTxt({ 19 | sitemap: [ 20 | "https://gianmarcocavallo.com/sitemap-index.xml", 21 | "https://gianmarcocavallo.com/sitemap-0.xml", 22 | ], 23 | }), 24 | solidJs(), 25 | UnoCSS({ injectReset: true }), 26 | icon(), 27 | svelte(), 28 | ], 29 | markdown: { 30 | remarkPlugins: [remarkReadingTime], 31 | }, 32 | output: "server", 33 | adapter: netlify({ edgeMiddleware: true }), 34 | vite: { 35 | assetsInclude: "**/*.riv", 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Gianmarco Cavallo and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/layouts/LayoutBlogPost.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { formatDate } from "../lib/helpers"; 3 | import BasicLayout from "./BasicLayout.astro"; 4 | 5 | type Props = { 6 | title: string; 7 | description: string; 8 | pubDate: Date; 9 | minutesRead: string; 10 | url: string; 11 | }; 12 | 13 | const { title, description, pubDate, minutesRead, url } = Astro.props; 14 | --- 15 | 16 | 17 |
    18 | Back 22 |
    23 |

    {title}

    24 |
    25 |

    {formatDate(pubDate)}

    26 |

    {minutesRead}

    27 |
    28 |
    29 |
    30 | 31 |
    32 |
    33 |
    34 | -------------------------------------------------------------------------------- /src/components/ContactsCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Card from "./Card/index.astro"; 3 | import { LINKS } from "../lib/constants"; 4 | --- 5 | 6 | 7 |
    8 |
    9 |

    10 | Let's start working together! 11 |

    12 |
    13 |
    14 |

    Contact Details

    15 |

    contact@gianmarcocavallo.com

    16 |

    Italy

    17 |
    18 |
    19 |

    Socials

    20 | 34 |
    35 |
    36 |
    37 | -------------------------------------------------------------------------------- /src/components/TimeZoneCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCurrentTimeInItaly, formatTimeForItaly } from "../lib/helpers"; 3 | import Card from "./Card/index.astro"; 4 | --- 5 | 6 | 30 | 31 | 32 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/AboutMe.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Card from "./Card/index.astro"; 3 | --- 4 | 5 | 6 |
    7 |
    8 |

    9 | Hi, I'm Gianmarco, a front-end software developer from Italy. 10 |
    11 | My primary tools of choice includes: 12 |

    13 |
      14 |
    • JavaScript
    • 15 |
    • React
    • 16 |
    • Solidjs
    • 17 |
    • Astro
    • 18 |
    • svelte
    • 19 |
    • Nodejs
    • 20 |
    21 |
    22 |

    23 | Beyond coding, I'm passionate about design, illustration, animation and 3D modelling and traveling. 24 | An unusual hobby of mine is collecting vintage passports, they're 25 | interesting pieces of history to me. 26 |

    27 |

    28 | While I have some preferred tools, I always choose the best one for the 29 | job, even if it's not on my usual list. My goal is to find the right 30 | solution for each project. 31 |

    32 |
    33 |
    34 | -------------------------------------------------------------------------------- /src/components/Card/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | import Content from "./Content.astro"; 4 | 5 | interface Props { 6 | title?: string; 7 | body?: string; 8 | colSpan?: string; 9 | rowSpan?: string; 10 | href?: string; 11 | colorText?: string; 12 | height?: string; 13 | width?: string; 14 | } 15 | 16 | const { title, body, colSpan, rowSpan, href, colorText, height } = Astro.props; 17 | --- 18 | 19 |
    26 | { 27 | href ? ( 28 | 29 | 32 | 33 | 34 | 35 | 36 | ) : ( 37 | 38 | 39 | 40 | ) 41 | } 42 |
    43 | -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | export function trimText(input: string, maxLength: number = 100): string { 2 | if (input.length <= maxLength) return input; 3 | return input.substring(0, maxLength - 3) + "..."; 4 | } 5 | export function getCurrentTimeInItaly(): Date { 6 | // Create a date object with the current UTC time 7 | const now = new Date(); 8 | 9 | // Convert the UTC time to Italy's time 10 | const offsetItaly = 2; // Italy is in Central European Summer Time (UTC+2), but you might need to adjust this based on Daylight Saving Time 11 | now.setHours(now.getUTCHours() + offsetItaly); 12 | 13 | return now; 14 | } 15 | 16 | export function formatTimeForItaly(date: Date): string { 17 | const options: Intl.DateTimeFormatOptions = { 18 | hour: "numeric", 19 | minute: "2-digit", 20 | second: "2-digit", 21 | hour12: true, // This will format the time in 12-hour format with AM/PM 22 | timeZone: "Europe/Rome", 23 | }; 24 | 25 | let formattedTime = new Intl.DateTimeFormat("en-US", options).format(date); 26 | 27 | // Append the time zone abbreviation. You can automate this with libraries like `moment-timezone`. 28 | // For simplicity, here I'm just appending "CET", but do remember that Italy switches between CET and CEST. 29 | formattedTime += " CET"; 30 | 31 | return formattedTime; 32 | } 33 | 34 | export function formatDate(date: Date): string { 35 | return date.toLocaleDateString("en-US", { 36 | year: "numeric", 37 | month: "long", 38 | day: "numeric", 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/blog/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from "astro:content"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | import PostRow from "../../components/Blog/PostRow.astro"; 5 | 6 | const posts = (await getCollection("blog"))?.sort( 7 | (blogEntryA, blogEntryB) => 8 | (blogEntryB.data.pubDate || new Date()).getTime() - 9 | (blogEntryA.data.pubDate || new Date()).getTime() 10 | ); 11 | --- 12 | 13 | 21 | 22 | 25 |
    28 | Back 32 |

    Posts

    33 |
      34 | { 35 | posts?.map((post) => ( 36 | 39 | )) 40 | } 41 |
    42 |
    43 |
    44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-bento-portfolio", 3 | "type": "module", 4 | "version": "0.0.2", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro", 11 | "eslint": "eslint 'src/**/*.{js,astro}'", 12 | "check": "astro check" 13 | }, 14 | "dependencies": { 15 | "@astrojs/check": "^0.9.6", 16 | "@astrojs/netlify": "^6.6.3", 17 | "@astrojs/rss": "^4.0.14", 18 | "@astrojs/sitemap": "^3.6.0", 19 | "@astrojs/solid-js": "^5.1.3", 20 | "@astrojs/svelte": "^7.2.2", 21 | "@iconify-json/ri": "^1.2.6", 22 | "@rive-app/canvas": "^2.32.2", 23 | "astro": "^5.16.4", 24 | "astro-icon": "^1.1.5", 25 | "astro-robots-txt": "^1.0.0", 26 | "autoprefixer": "^10.4.22", 27 | "d3": "^7.9.0", 28 | "gsap": "^3.13.0", 29 | "lenis": "^1.3.15", 30 | "mdast-util-to-string": "^4.0.0", 31 | "motion": "^12.23.25", 32 | "reading-time": "^1.5.0", 33 | "solid-js": "^1.9.10", 34 | "svelte": "^5.45.6" 35 | }, 36 | "devDependencies": { 37 | "@types/d3": "^7.4.3", 38 | "@typescript-eslint/parser": "^8.48.1", 39 | "@unocss/astro": "^66.5.10", 40 | "@unocss/postcss": "^66.5.10", 41 | "@unocss/preset-uno": "^66.5.10", 42 | "@unocss/reset": "^66.5.10", 43 | "autoprefixer": "^10.4.22", 44 | "eslint": "^9.39.1", 45 | "eslint-plugin-astro": "^1.5.0", 46 | "eslint-plugin-jsx-a11y": "^6.10.2", 47 | "markdown-it": "^14.1.0", 48 | "motion": "^12.23.25", 49 | "sanitize-html": "^2.17.0", 50 | "typescript": "^5.9.3", 51 | "unocss": "^66.5.10" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | // uno.config.ts 2 | import { defineConfig, presetWind3, presetWebFonts } from "unocss"; 3 | 4 | export default defineConfig({ 5 | content: { 6 | filesystem: [ 7 | // Narrow scope to specific directories 8 | "src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}}", 9 | "src/components/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}}", 10 | "src/pages/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}}", 11 | "src/layouts/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}}" 12 | ], }, 13 | theme: { 14 | boxShadow: { 15 | custom: `2px 2px 0`, 16 | "custom-hover": `1px 1px 0`, 17 | }, 18 | fontFamily: { 19 | sans: ["CabinetGrotesk", "Satoshi"], 20 | }, 21 | gridTemplateRows: { 22 | "auto-250": "repeat(auto-fill, 250px)", 23 | }, 24 | gridTemplateColumns: { 25 | "4-minmax": "repeat(4, minmax(150px, 1fr))", 26 | }, 27 | colors: { 28 | gray: { 29 | 50: "#FAFAFA", 30 | 100: "#F5F5F5", 31 | 200: "#E5E5E5", 32 | 300: "#D4D4D4", 33 | 400: "#A3A3A3", 34 | 500: "#737373", 35 | 600: "#525252", 36 | 700: "#404040", 37 | 800: "#262626", 38 | 900: "#171717", 39 | }, 40 | darkslate: { 41 | 50: "#3D3D3D", 42 | 100: "#2C2C2C", 43 | 200: "#262626", 44 | 300: "#202020", 45 | 400: "#1A1A1A", 46 | 500: "#171717" /* Exactly your example for the background */, 47 | 600: "#141414", 48 | 700: "#111111", 49 | 800: "#0E0E0E", 50 | 900: "#0B0B0B" /* Deeper and darker */, 51 | }, 52 | primary: { 53 | 100: "#F9CDD3", 54 | 200: "#F3A3AA", 55 | 300: "#EC7981", 56 | 400: "#E64F59", 57 | 500: "#E63946", 58 | 600: "#CF2F3D", 59 | 700: "#B82534", 60 | 800: "#A01B2B", 61 | 900: "#891321", 62 | }, 63 | }, 64 | }, 65 | presets: [ 66 | presetWind3(), 67 | presetWebFonts({ 68 | provider: "fontshare", 69 | fonts: { 70 | sans: ["Cabinet Grotesk", "Satoshi"], 71 | serif: "Zodiak", 72 | }, 73 | }), 74 | ], 75 | }); 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️astro-bento-portfolio 2 | 3 | ## A personal portfolio website made using `Astro`. 4 | 5 | ![astro-bento-portfolio | Bento-like Personal Porfolio Template](public/preview.png) 6 | 7 | To view a demo example, **[click here](https://sparkly-speculoos-0c9197.netlify.app/)** 8 | 9 | or my portfolio **[click here](https://gianmarcocavallo.com)** 10 | 11 | ## Features 12 | 13 | - Modern and Minimal bento-like, sleek UI Design 14 | - All in one page (almost) 15 | - Fully Responsive 16 | - Performances and SEO optimizations 17 | - Ready to be deployed on [Netlify](https://www.netlify.com/) 18 | - Blog 19 | - RSS support (your-domain/rss.xml) 20 | - Cool 3d globe 21 | 22 | ## Tech Stack 23 | 24 | - [Astro](https://astro.build) 25 | - [unocss](https://unocss.dev/) 26 | - [motion](https://motion.dev/) 27 | - [d3](https://d3js.org/) 28 | 29 | # Steps ▶️ 30 | 31 | ```bash 32 | # Clone this repository 33 | $ git clone https://github.com/Ladvace/astro-bento-portfolio 34 | ``` 35 | 36 | ```bash 37 | # Go into the repository 38 | $ cd astro-bento-portfolio 39 | ``` 40 | 41 | ```bash 42 | # Install dependencies 43 | $ pnpm install 44 | or 45 | $ npm install 46 | ``` 47 | 48 | ```bash 49 | # Start the project in development 50 | $ pnpm run dev 51 | or 52 | $ npm run dev 53 | ``` 54 | 55 | # Be sure to replace the momoji and all the relative information, such as email, website and other info, if you don't your website is gonna point to my domain and to my info 56 | 57 | ## REMOVE THE umami analytics script tag (or replace it with your id) in `src/layouts/Layout.astro` 58 | 59 | # Configuration 60 | 61 | remember to replace the `site` and other properties with your data in `astro.config.mjs` 62 | 63 | # Deploy on Netlify 🚀 64 | 65 | Deploying your website on Netlify it's optional but I reccomand it in order to deploy it faster and easly. 66 | 67 | You just need to fork this repo and linking it to your Netlify account. 68 | 69 | or 70 | 71 | [![Netlify Deploy button](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/Ladvace/astro-bento-portfolio) 72 | 73 | ## Authors ❤️ 74 | 75 | - Gianmarco - https://github.com/Ladvace 76 | -------------------------------------------------------------------------------- /src/components/Globe.tsx: -------------------------------------------------------------------------------- 1 | import { onMount } from "solid-js"; 2 | import * as d3 from "d3"; 3 | import worldData from "../lib/world.json"; 4 | 5 | const GlobeComponent = () => { 6 | let mapContainer: HTMLDivElement | undefined; 7 | 8 | const visitedCountries = [ 9 | "France", 10 | "China", 11 | "Italy", 12 | "Sri Lanka", 13 | "Turkey", 14 | "Greece", 15 | "Malta", 16 | "Hungary", 17 | "Portugal", 18 | "Marocco", 19 | ]; 20 | 21 | onMount(() => { 22 | if (!mapContainer) return; 23 | 24 | const width = mapContainer.clientWidth; 25 | const height = 500; 26 | const sensitivity = 75; 27 | 28 | let projection = d3 29 | .geoOrthographic() 30 | .scale(250) 31 | .center([0, 0]) 32 | .rotate([0, -30]) 33 | .translate([width / 2, height / 2]); 34 | 35 | const initialScale = projection.scale(); 36 | let pathGenerator = d3.geoPath().projection(projection); 37 | 38 | let svg = d3 39 | .select(mapContainer) 40 | .append("svg") 41 | .attr("width", width) 42 | .attr("height", height); 43 | 44 | svg 45 | .append("circle") 46 | .attr("fill", "#EEE") 47 | .attr("stroke", "#000") 48 | .attr("stroke-width", "0.2") 49 | .attr("cx", width / 2) 50 | .attr("cy", height / 2) 51 | .attr("r", initialScale); 52 | 53 | let map = svg.append("g"); 54 | 55 | map 56 | .append("g") 57 | .attr("class", "countries") 58 | .selectAll("path") 59 | .data(worldData.features) 60 | .enter() 61 | .append("path") 62 | .attr("d", (d: any) => pathGenerator(d as any)) 63 | .attr("fill", (d: { properties: { name: string } }) => 64 | visitedCountries.includes(d.properties.name) ? "#E63946" : "white" 65 | ) 66 | .style("stroke", "black") 67 | .style("stroke-width", 0.3) 68 | .style("opacity", 0.8); 69 | 70 | d3.timer(() => { 71 | const rotate = projection.rotate(); 72 | const k = sensitivity / projection.scale(); 73 | projection.rotate([rotate[0] - 1 * k, rotate[1]]); 74 | svg.selectAll("path").attr("d", (d: any) => pathGenerator(d as any)); 75 | }, 200); 76 | }); 77 | 78 | return ( 79 |
    80 |
    81 |
    82 | ); 83 | }; 84 | 85 | export default GlobeComponent; 86 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX, Show, createSignal } from "solid-js"; 2 | 3 | type Props = { 4 | children: JSX.Element; 5 | }; 6 | 7 | function Tooltip(props: Props) { 8 | const [isVisible, setIsVisible] = createSignal(false); 9 | const [clickCount, setClickCount] = createSignal(0); 10 | 11 | const messages = [ 12 | "Hi there!", 13 | "Clicked again?", 14 | "Still here?", 15 | "Persistent, aren't you?", 16 | "What's up?", 17 | "Again? Really?", 18 | "You're curious!", 19 | "Not cool!", 20 | "Give it a break!", 21 | "That's annoying!", 22 | "Hands off!", 23 | "No more clicks!", 24 | "Seriously?!", 25 | "Ouch! That hurts!", 26 | "You're persistent!", 27 | "Why the curiosity?", 28 | "I'm getting tired!", 29 | "I'm bored!", 30 | "Enough's enough!", 31 | "Find another hobby!", 32 | "Stop, please!", 33 | "Okay, last one!", 34 | "That's it, I'm done!", 35 | ]; 36 | 37 | const currentMessage = () => { 38 | const count = clickCount(); 39 | if (count >= messages.length) { 40 | return messages[messages.length - 1]; 41 | } 42 | return messages[count]; 43 | }; 44 | 45 | return ( 46 |
    47 |
    { 49 | setIsVisible(!isVisible()); 50 | if (isVisible()) { 51 | setClickCount((count) => count + 1); 52 | } 53 | }} 54 | onMouseUp={() => { 55 | setIsVisible(false); 56 | }} 57 | onTouchStart={() => { 58 | setIsVisible(!isVisible()); 59 | if (isVisible()) { 60 | setClickCount((count) => count + 1); 61 | } 62 | }} 63 | onTouchEnd={() => { 64 | setIsVisible(false); 65 | }} 66 | > 67 | {props.children} 68 |
    69 | 70 | 71 |
    72 |

    {currentMessage()}

    73 |
    74 |
    75 |
    76 | ); 77 | } 78 | 79 | export default Tooltip; 80 | -------------------------------------------------------------------------------- /src/pages/blog/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getEntry, render } from "astro:content"; 3 | import { getCollection } from "astro:content"; 4 | import LayoutBlogPost from "../../layouts/LayoutBlogPost.astro"; 5 | 6 | export const prerender = true; 7 | 8 | export async function getStaticPaths() { 9 | return (await getCollection("blog")).map(({ id }) => ({ 10 | params: { id: id as string }, 11 | })); 12 | } 13 | 14 | const { id } = Astro.params; 15 | 16 | if (!id) { 17 | throw new Error("id is missing"); 18 | } 19 | 20 | const entry = await getEntry("blog", id); 21 | 22 | if (!entry) { 23 | return Astro.redirect("/404"); 24 | } 25 | 26 | const { Content, remarkPluginFrontmatter } = await render(entry); 27 | --- 28 | 29 | 32 | 33 | 34 | 35 | 131 | -------------------------------------------------------------------------------- /src/components/IntroCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Card from "./Card/index.astro"; 3 | import Button from "./Button.astro"; 4 | import { LINKS } from "../lib/constants"; 5 | import { Icon } from "astro-icon/components"; 6 | import Tooltip from "./Tooltip/index"; 7 | import { Image } from "astro:assets"; 8 | --- 9 | 10 | 11 |
    12 |
    13 |
    14 |
    welcome
    15 |

    16 | Hi, I'm Gianmarco Cavallo, a software 17 | developer with strong focus on the user experience, animations and 18 | micro interactions 19 |

    20 |
    21 | 55 |
    56 | memoji of gianmarco 63 |
    64 |
    65 | -------------------------------------------------------------------------------- /src/components/playground/scroll-1/scroll-1.svelte: -------------------------------------------------------------------------------- 1 | 67 | 68 | 69 | Home 70 | 71 | 72 | 73 |
    74 | 75 | 76 | 77 |
    78 | 79 | 80 |
    83 |
    84 | 85 | 86 | 87 |
    88 |
    89 | 90 | 91 | 92 |
    93 |
    94 | 95 | 96 | 97 |
    98 |
    99 | 100 | 108 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../layouts/Layout.astro"; 3 | import Card from "../components/Card/index.astro"; 4 | import IntroCard from "../components/IntroCard.astro"; 5 | import ContactsCard from "../components/ContactsCard.astro"; 6 | import TimeZone from "../components/TimeZoneCard.astro"; 7 | import AboutMe from "../components/AboutMe.astro"; 8 | import Now from "../components/Now.astro"; 9 | import Globe from "../components/Globe"; 10 | import { getCollection } from "astro:content"; 11 | import { formatDate } from "../lib/helpers"; 12 | 13 | const posts = (await getCollection("blog"))?.sort( 14 | (blogEntryA, blogEntryB) => 15 | (blogEntryB.data.pubDate || new Date()).getTime() - 16 | (blogEntryA.data.pubDate || new Date()).getTime() 17 | ); 18 | --- 19 | 20 | 40 | 41 | 45 |
    48 | 49 | 50 | 51 | 52 | 53 | 60 |
    61 | 62 |
    63 |
    64 | 70 | 95 | 96 | 97 |

    98 | © 2024 · Crafted with ♥️ using Astro by Gianmarco. 103 |

    104 |
    105 |
    106 |
    107 | -------------------------------------------------------------------------------- /src/components/playground/rifle-1.tsx: -------------------------------------------------------------------------------- 1 | import * as rive from "@rive-app/canvas"; 2 | import rifleAnimation from "../../riveAnimations/rifle.riv"; 3 | import { createSignal, onCleanup, onMount } from "solid-js"; 4 | 5 | const Illustrations = () => { 6 | let canvas: HTMLCanvasElement | undefined; 7 | const [riveRef, setRiveRef] = createSignal(); 8 | const [keepShootingInput, setKeepShootingInput] = createSignal< 9 | rive.StateMachineInput | undefined 10 | >(); 11 | // Signals for canvas dimensions 12 | const [canvasWidth, setCanvasWidth] = createSignal(950); 13 | const [canvasHeight, setCanvasHeight] = createSignal(540); 14 | 15 | const updateCanvasSize = () => { 16 | const screenWidth = window.innerWidth; 17 | // Assuming you want the canvas to take up most of the screen width but maintain its aspect ratio 18 | const newCanvasWidth = Math.min(screenWidth * 0.9, 950); // Cap at original width or 90% of screen width 19 | console.log("TEST", screenWidth, newCanvasWidth); 20 | const aspectRatio = 950 / 540; 21 | const newCanvasHeight = newCanvasWidth / aspectRatio; 22 | setCanvasWidth(newCanvasWidth); 23 | setCanvasHeight(newCanvasHeight); 24 | }; 25 | 26 | onMount(() => { 27 | updateCanvasSize(); // Initial size update 28 | window.addEventListener("resize", updateCanvasSize); // Update on resize 29 | 30 | const keepShootingCheckbox = document.getElementById( 31 | "keep-shooting" 32 | ) as HTMLInputElement; 33 | 34 | const handleOnChange = () => { 35 | const input = keepShootingInput(); 36 | if (input) { 37 | input.value = keepShootingCheckbox.checked; 38 | } 39 | }; 40 | 41 | if (canvas && rifleAnimation) { 42 | const r = new rive.Rive({ 43 | src: rifleAnimation, 44 | autoplay: true, 45 | canvas: canvas, 46 | stateMachines: ["rifle"], 47 | onLoad: () => { 48 | r.resizeDrawingSurfaceToCanvas(); 49 | setRiveRef(r); 50 | const inputs = r.stateMachineInputs("rifle"); 51 | const keep_shooting = inputs?.find((i) => i.name === "keep_shooting"); 52 | if (keep_shooting) { 53 | setKeepShootingInput(keep_shooting); 54 | } 55 | 56 | if (keepShootingCheckbox) { 57 | keepShootingCheckbox.addEventListener("change", handleOnChange); 58 | } 59 | }, 60 | }); 61 | } 62 | 63 | onCleanup(() => { 64 | window.removeEventListener("resize", updateCanvasSize); 65 | if (riveRef()) { 66 | riveRef()?.stopRendering(); 67 | riveRef()?.cleanup(); 68 | } 69 | if (keepShootingCheckbox) { 70 | keepShootingCheckbox.removeEventListener("change", handleOnChange); 71 | } 72 | }); 73 | }); 74 | 75 | return ( 76 |
    77 |

    Rifle animation

    78 |

    79 | Interactive animation made in rive.app using an 80 | illustration made by me 81 |

    82 |

    83 | click on the rifle or on the S and on the{" "} 84 | R to reload 85 |

    86 | 87 |
    88 |
    89 | 94 | 95 |
    96 |
    97 | { 99 | canvas = el; 100 | }} 101 | width={canvasWidth()} 102 | height={canvasHeight()} 103 | style={{ 104 | width: `${canvasWidth()}px`, 105 | height: `${canvasHeight()}px`, 106 | }} 107 | /> 108 |
    109 |
    110 |
    111 | ); 112 | }; 113 | 114 | export default Illustrations; 115 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { LINKS } from "../lib/constants"; 3 | 4 | interface Props { 5 | title: string; 6 | description: string; 7 | page?: "travel" | "blog"; 8 | slug?: string | undefined; 9 | pubDate?: string; 10 | } 11 | 12 | const { title, description, page, pubDate } = Astro.props; 13 | const image = 14 | page === "travel" 15 | ? `${Astro.url.origin}/globe_preview.webp"` 16 | : `${Astro.url.origin}/og-image.png`; 17 | 18 | const schema = 19 | page !== "blog" 20 | ? { 21 | "@context": "http://schema.org", 22 | "@type": "Person", 23 | name: "Gianmarco", 24 | url: Astro.url.origin, 25 | sameAs: [LINKS.linkedin, LINKS.github], 26 | image: `${Astro.url.origin}/og-image.png`, 27 | jobTitle: "Freelance Frontend Developer", 28 | worksFor: { 29 | "@type": "Organization", 30 | name: "Self-Employed", 31 | address: { 32 | "@type": "PostalAddress", 33 | addressLocality: "Italy", 34 | addressCountry: "IT", 35 | }, 36 | }, 37 | nationality: { 38 | "@type": "Country", 39 | name: "Italy", 40 | }, 41 | } 42 | : { 43 | "@context": "http://schema.org", 44 | "@type": "BlogPosting", 45 | mainEntityOfPage: { 46 | "@type": "WebPage", 47 | "@id": Astro.url.href, 48 | }, 49 | headline: title || title, 50 | description: description || title, 51 | image: `${Astro.url.origin}/og-image.png`, //TODO: dynamic 52 | author: { 53 | "@type": "Person", 54 | name: "Gianmarco", 55 | url: Astro.url.origin, 56 | sameAs: [LINKS.linkedin, LINKS.github], 57 | }, 58 | publisher: { 59 | "@type": "Organization", 60 | name: "Gianmarco", 61 | logo: { 62 | "@type": "ImageObject", 63 | url: `${Astro.url.origin}/og-image.png`, 64 | }, 65 | }, 66 | datePublished: pubDate || new Date().toISOString(), 67 | dateModified: pubDate || new Date().toISOString(), 68 | }; 69 | --- 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {title} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 90 | 91 | 92 | 93 | 94 | 98 | 99 | 100 | 101 |