@minch/bentolio 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/app/app.config.ts +53 -0
- package/app/app.vue +156 -0
- package/app/assets/css/main.css +85 -0
- package/app/components/AppFooter.vue +5 -0
- package/app/components/AppHeader.vue +27 -0
- package/app/components/OgImage/SocialImage.vue +31 -0
- package/app/components/SocialMediaLink.vue +30 -0
- package/app/components/ThemeToggle.vue +26 -0
- package/app/index.d.ts +1 -0
- package/app/layouts/default.vue +73 -0
- package/app/plugins/gsap.client.ts +9 -0
- package/app/utils/types.ts +42 -0
- package/eslint.config.mjs +11 -0
- package/nuxt.config.ts +51 -0
- package/package.json +61 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/favicon.svg +6 -0
- package/public/logo.svg +6 -0
- package/public/profile.jpg +0 -0
- package/public/robots.txt +2 -0
- package/public/screenshot.png +0 -0
package/.env.example
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dawit Urgessa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Bentolio
|
|
2
|
+
|
|
3
|
+
A minimal portfolio template built with Nuxt 4, Tailwind CSS 4, and GSAP.
|
|
4
|
+
|
|
5
|
+
All content and basic styling are configured via `app/app.config.ts`.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
> For the original Astro version (not actively updated), check the [`astro`](https://github.com/oneminch/Bentolio/tree/astro) branch.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Nuxt 4**: Modern DX, serverless-friendly, and SSG by default
|
|
14
|
+
- **Tailwind CSS 4**: Zero-config utility classes with CSS-first authoring
|
|
15
|
+
- **GSAP animations**: Smooth entrance animations and micro‑interactions
|
|
16
|
+
- **Dark Mode**: Toggle via `@nuxtjs/color-mode` (class strategy)
|
|
17
|
+
- **Icons**: `@nuxt/icon` with Iconify collections (e.g., `ph`, `simple-icons`)
|
|
18
|
+
- **Type‑safe app config**: Autocompletion and validation for your site data
|
|
19
|
+
- **Static generation**: Pre‑rendered pages for easy deployment to any static host
|
|
20
|
+
- **OG Image**: Automatically generated OG image via `nuxt-og-image`
|
|
21
|
+
|
|
22
|
+
## Project Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
app/
|
|
26
|
+
app.config.ts # Portfolio data and style options (edit me)
|
|
27
|
+
app.vue # Single-page portfolio content using app config
|
|
28
|
+
assets/css/main.css # Tailwind entry + custom theme/utilities
|
|
29
|
+
components/ # UI components (Header, Footer, Social link, Theme toggle)
|
|
30
|
+
layouts/default.vue # Layout + SEO meta + GSAP entrance animations
|
|
31
|
+
plugins/gsap.client.ts # Nuxt plugin providing `$gsap`
|
|
32
|
+
utils/types.ts # Types for the app config
|
|
33
|
+
public/
|
|
34
|
+
favicon.svg, logo.svg, profile.jpg, robots.txt
|
|
35
|
+
nuxt.config.ts # Nuxt configuration
|
|
36
|
+
package.json # Scripts and dependencies
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configure Your Portfolio
|
|
40
|
+
|
|
41
|
+
All site data lives in `app/app.config.ts` and is fully typed (see `app/utils/types.ts`). Edit the example values to your own:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// app/app.config.ts (excerpt)
|
|
45
|
+
export default defineAppConfig({
|
|
46
|
+
style: {
|
|
47
|
+
roundedItems: false
|
|
48
|
+
},
|
|
49
|
+
portfolio: {
|
|
50
|
+
name: "Your Name",
|
|
51
|
+
subtitle: "What you do",
|
|
52
|
+
// company: "[COMPANY]",
|
|
53
|
+
avatar: "/profile.jpg",
|
|
54
|
+
bio: {
|
|
55
|
+
// Prefer html for rich content; fallback to text if html is empty
|
|
56
|
+
html: "I'm a <span class='font-bold text-primary'>Full‑Stack</span> Dev...",
|
|
57
|
+
text: "I'm a Full‑Stack Dev..."
|
|
58
|
+
},
|
|
59
|
+
socials: {
|
|
60
|
+
GitHub: {
|
|
61
|
+
socialMediaIcon: "simple-icons:github",
|
|
62
|
+
profileUrl: "https://github.com/your-handle",
|
|
63
|
+
socialMediaBgColor: "bg-black!",
|
|
64
|
+
socialMediaBorderColor: "border-black!",
|
|
65
|
+
socialMediaTextColor: "text-white!"
|
|
66
|
+
}
|
|
67
|
+
// X, LinkedIn, Bluesky are also supported
|
|
68
|
+
},
|
|
69
|
+
cta: {
|
|
70
|
+
contact: "you@example.com",
|
|
71
|
+
location: "Your City",
|
|
72
|
+
link: { url: "https://your-site.com", label: "Portfolio" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Supported social keys by default: `"X" | "LinkedIn" | "Bluesky" | "GitHub"`. To add more networks, extend the `SocialMediaKey` union and the `socials` mapping in `app/utils/types.ts`. The layout is designed to only display 4 social links by default.
|
|
79
|
+
|
|
80
|
+
### Assets
|
|
81
|
+
|
|
82
|
+
- Replace `public/profile.jpg` with your own avatar.
|
|
83
|
+
- Update `public/favicon.svg` and `public/logo.svg` if desired.
|
|
84
|
+
- OG image is auto-generated using the `nuxt-og-image` module.
|
|
85
|
+
|
|
86
|
+
### Theming and Styles
|
|
87
|
+
|
|
88
|
+
- Tailwind entry is `app/assets/css/main.css`.
|
|
89
|
+
- Primary color is set via CSS theme variable:
|
|
90
|
+
|
|
91
|
+
```css
|
|
92
|
+
/* Change `--color-primary` to customize your accent. */
|
|
93
|
+
@theme {
|
|
94
|
+
--color-primary: var(--color-lime-500);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Animations
|
|
99
|
+
|
|
100
|
+
- GSAP is provided via a Nuxt plugin (`$gsap`). See `app/layouts/default.vue` for entrance animations.
|
|
101
|
+
- Elements with the `animate-element` utility class will animate in on page load. The initial state and transitions are defined in `app/assets/css/main.css`.
|
|
102
|
+
- The waving hand emoji animation is controlled via the `#wave` id in CSS.
|
|
103
|
+
|
|
104
|
+
## Deployment
|
|
105
|
+
|
|
106
|
+
This template is configured for static generation via `routeRules` in `nuxt.config.ts`.
|
|
107
|
+
|
|
108
|
+
## Credit
|
|
109
|
+
|
|
110
|
+
The design is inspired by [@TomIsLoading](https://twitter.com/TomIsLoading)'s Bento Grid from [Hover.dev](https://hover.dev).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export default defineAppConfig({
|
|
2
|
+
style: {
|
|
3
|
+
roundedItems: false,
|
|
4
|
+
},
|
|
5
|
+
portfolio: {
|
|
6
|
+
name: "Dawit",
|
|
7
|
+
subtitle: "I Craft Delightful Web Apps",
|
|
8
|
+
// company: "[COMPANY]",
|
|
9
|
+
avatar: "/profile.jpg",
|
|
10
|
+
bio: {
|
|
11
|
+
html: "I'm a Full-Stack Problem-Solver specializing in <span class='font-bold text-primary'>Vue</span>, <span class='font-bold text-primary'>Nuxt</span>, <span class='font-bold text-primary'>Tailwind CSS</span>, and <span class='font-bold text-primary'>Node</span>. I sometimes write articles on various software development topics. I'm also a hobbyist photographer.",
|
|
12
|
+
text: "I'm a Full-Stack Problem-Solver Specializing in Vue, Nuxt, Tailwind CSS and Node.js. I Sometimes Write Articles on Various Web Dev Topics. I'm Also a Hobbyist Photographer.",
|
|
13
|
+
},
|
|
14
|
+
socials: {
|
|
15
|
+
Bluesky: {
|
|
16
|
+
socialMediaIcon: "simple-icons:bluesky",
|
|
17
|
+
profileUrl: "https://bsky.app/profile/minch.dev",
|
|
18
|
+
socialMediaBgColor: "bg-blue-500!",
|
|
19
|
+
socialMediaBorderColor: "border-blue-500!",
|
|
20
|
+
socialMediaTextColor: "text-white!",
|
|
21
|
+
},
|
|
22
|
+
GitHub: {
|
|
23
|
+
socialMediaIcon: "simple-icons:github",
|
|
24
|
+
profileUrl: "https://x.com/oneminch",
|
|
25
|
+
socialMediaBgColor: "bg-black!",
|
|
26
|
+
socialMediaBorderColor: "border-black!",
|
|
27
|
+
socialMediaTextColor: "text-white!",
|
|
28
|
+
},
|
|
29
|
+
X: {
|
|
30
|
+
socialMediaIcon: "simple-icons:x",
|
|
31
|
+
profileUrl: "https://x.com/oneminch",
|
|
32
|
+
socialMediaBgColor: "bg-black!",
|
|
33
|
+
socialMediaBorderColor: "border-black!",
|
|
34
|
+
socialMediaTextColor: "text-white!",
|
|
35
|
+
},
|
|
36
|
+
LinkedIn: {
|
|
37
|
+
socialMediaIcon: "simple-icons:linkedin",
|
|
38
|
+
profileUrl: "https://linkedin.com/in/oneminch",
|
|
39
|
+
socialMediaBgColor: "bg-blue-600!",
|
|
40
|
+
socialMediaBorderColor: "border-blue-600!",
|
|
41
|
+
socialMediaTextColor: "text-white!",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
cta: {
|
|
45
|
+
contact: "[firstname]@example.com",
|
|
46
|
+
location: "Planet Earth",
|
|
47
|
+
link: {
|
|
48
|
+
url: "https://github.com/oneminch/Bentolio",
|
|
49
|
+
label: "Source Code",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SocialMediaKey } from "~";
|
|
3
|
+
|
|
4
|
+
const { portfolio, style } = useAppConfig();
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
name,
|
|
8
|
+
subtitle,
|
|
9
|
+
avatar,
|
|
10
|
+
company,
|
|
11
|
+
bio,
|
|
12
|
+
cta: { contact, location, link },
|
|
13
|
+
socials
|
|
14
|
+
} = portfolio;
|
|
15
|
+
|
|
16
|
+
const { roundedItems } = style;
|
|
17
|
+
|
|
18
|
+
const topFourSocials = Object.fromEntries(
|
|
19
|
+
Object.entries(socials).slice(0, 4)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const numSocials = Object.entries(topFourSocials).length;
|
|
23
|
+
const socialLinkClass = {
|
|
24
|
+
"col-span-full lg:col-span-4 lg:row-span-2": numSocials === 1,
|
|
25
|
+
"col-span-6 lg:col-span-4": numSocials === 2,
|
|
26
|
+
"col-span-4 last:col-span-4 lg:col-span-2 lg:last:col-span-4":
|
|
27
|
+
numSocials === 3,
|
|
28
|
+
"col-span-3 lg:col-span-2": numSocials === 4
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
defineOgImageComponent("SocialImage", {
|
|
32
|
+
name: name,
|
|
33
|
+
subtitle: subtitle,
|
|
34
|
+
avatar: avatar
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<NuxtRouteAnnouncer />
|
|
40
|
+
<NuxtLayout>
|
|
41
|
+
<article
|
|
42
|
+
class="flex flex-col gap-4 *:grid *:grid-cols-12 *:gap-4 *:w-full *:*:min-h-16 *:*:py-4 *:*:px-6 sm:*:*:py-6 sm:*:*:px-10 *:*:border *:*:border-zinc-200 dark:*:*:border-zinc-700 *:*:bg-zinc-50/50 dark:*:*:bg-zinc-800/25 *:text-zinc-950 dark:*:text-zinc-300">
|
|
43
|
+
<!-- Hero -->
|
|
44
|
+
<div class="grid-rows-2">
|
|
45
|
+
<!-- Intro -->
|
|
46
|
+
<div
|
|
47
|
+
:class="[
|
|
48
|
+
'animate-element col-span-full lg:col-span-8 row-span-2',
|
|
49
|
+
{ 'rounded-lg': roundedItems }
|
|
50
|
+
]">
|
|
51
|
+
<img
|
|
52
|
+
v-if="avatar"
|
|
53
|
+
:src="avatar"
|
|
54
|
+
alt="My Profile Picture"
|
|
55
|
+
class="size-20 mb-4 mx-auto lg:mx-0 bg-transparent rounded-full ring ring-zinc-200 dark:ring-zinc-800 p-0.5" />
|
|
56
|
+
|
|
57
|
+
<div class="space-y-4 py-6 sm:pb-8">
|
|
58
|
+
<h1
|
|
59
|
+
class="text-2xl sm:text-3xl lg:text-4xl text-center lg:text-left font-bold *:inline-block">
|
|
60
|
+
Hi, I'm {{ name }}
|
|
61
|
+
<span id="wave">👋</span>
|
|
62
|
+
</h1>
|
|
63
|
+
<h2
|
|
64
|
+
class="text-center lg:text-left font-bold *:inline-block text-zinc-500 dark:text-primary text-xl sm:text-2xl lg:text-3xl">
|
|
65
|
+
<span>{{ subtitle }}</span
|
|
66
|
+
><span v-if="company"> @ {{ company }}</span>
|
|
67
|
+
</h2>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Social Media -->
|
|
72
|
+
<social-media-link
|
|
73
|
+
v-for="(item, key) in topFourSocials"
|
|
74
|
+
:key="key"
|
|
75
|
+
:to="item.profileUrl"
|
|
76
|
+
:name="key as SocialMediaKey"
|
|
77
|
+
:icon="item.socialMediaIcon"
|
|
78
|
+
:style="{
|
|
79
|
+
backgroundColor: item.socialMediaBgColor,
|
|
80
|
+
textColor: item.socialMediaTextColor,
|
|
81
|
+
borderColor: item.socialMediaBorderColor
|
|
82
|
+
}"
|
|
83
|
+
:class="[
|
|
84
|
+
'animate-element',
|
|
85
|
+
socialLinkClass,
|
|
86
|
+
{ 'rounded-lg': roundedItems }
|
|
87
|
+
]" />
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- About -->
|
|
91
|
+
<div class="grid-rows-1">
|
|
92
|
+
<p
|
|
93
|
+
v-if="bio.html"
|
|
94
|
+
:class="[
|
|
95
|
+
'animate-element leading-relaxed col-span-full font-medium gap-4 py-6! sm:py-10! text-zinc-700 dark:text-zinc-300 text-lg sm:text-xl lg:text-2xl',
|
|
96
|
+
{ 'rounded-lg': roundedItems }
|
|
97
|
+
]"
|
|
98
|
+
v-html="bio.html"></p>
|
|
99
|
+
<p
|
|
100
|
+
v-else
|
|
101
|
+
:class="[
|
|
102
|
+
'animate-element leading-relaxed col-span-full font-medium gap-4 py-6! sm:py-10! text-zinc-700 dark:text-zinc-300 text-lg sm:text-xl lg:text-2xl',
|
|
103
|
+
{ 'rounded-lg': roundedItems }
|
|
104
|
+
]">
|
|
105
|
+
{{ bio.text }}
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Miscellany -->
|
|
110
|
+
<div
|
|
111
|
+
class="grid-rows-1 grid-cols-1! sm:grid-cols-[repeat(auto-fit,minmax(100px,1fr))]!">
|
|
112
|
+
<!-- Link -->
|
|
113
|
+
<nuxt-link
|
|
114
|
+
v-if="link"
|
|
115
|
+
:to="link.url"
|
|
116
|
+
:class="[
|
|
117
|
+
'animate-element flex flex-col items-center justify-center gap-y-2 focus-visible:global-focus hover:scale-105!',
|
|
118
|
+
{ 'rounded-lg': roundedItems }
|
|
119
|
+
]">
|
|
120
|
+
<Icon
|
|
121
|
+
name="ph:arrow-square-up-right-duotone"
|
|
122
|
+
class="text-3xl lg:text-4xl text-primary" />
|
|
123
|
+
<span class="text-center text-lg">
|
|
124
|
+
{{ link.label }}
|
|
125
|
+
</span>
|
|
126
|
+
</nuxt-link>
|
|
127
|
+
|
|
128
|
+
<!-- Location -->
|
|
129
|
+
<p
|
|
130
|
+
:class="[
|
|
131
|
+
'animate-element flex flex-col items-center justify-center gap-y-2',
|
|
132
|
+
{ 'rounded-lg': roundedItems }
|
|
133
|
+
]">
|
|
134
|
+
<Icon
|
|
135
|
+
name="ph:map-pin-duotone"
|
|
136
|
+
class="text-3xl lg:text-4xl text-primary" />
|
|
137
|
+
<span class="text-center text-lg"> {{ location }} </span>
|
|
138
|
+
</p>
|
|
139
|
+
|
|
140
|
+
<!-- Contact -->
|
|
141
|
+
<p
|
|
142
|
+
:class="[
|
|
143
|
+
'animate-element flex flex-col items-center justify-center gap-y-2',
|
|
144
|
+
{ 'rounded-lg': roundedItems }
|
|
145
|
+
]">
|
|
146
|
+
<Icon
|
|
147
|
+
name="ph:chat-circle-text-duotone"
|
|
148
|
+
class="text-3xl lg:text-4xl text-primary" />
|
|
149
|
+
<span class="text-center text-lg">
|
|
150
|
+
{{ contact }}
|
|
151
|
+
</span>
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</article>
|
|
155
|
+
</NuxtLayout>
|
|
156
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
--color-primary: var(--color-lime-500);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@utility global-focus {
|
|
9
|
+
@apply outline-none ring-2 ring-offset-0 ring-primary/75;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@utility animate-element {
|
|
13
|
+
@apply transition-transform opacity-0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* {
|
|
17
|
+
@apply box-border selection:bg-primary/50;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
font-family: "Space Grotesk", sans-serif;
|
|
22
|
+
word-wrap: break-word;
|
|
23
|
+
@apply w-full bg-white dark:bg-zinc-900;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#app {
|
|
27
|
+
@apply max-w-5xl min-w-xs w-full bg-white dark:bg-zinc-900 mx-auto break-words my-0 p-4 sm:p-6 md:p-12;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Animated Wave Emoji */
|
|
31
|
+
#wave {
|
|
32
|
+
@apply inline-block ml-1 origin-[70%_70%] animate-[10s_ease_2s_infinite_wave] hover:animate-[1.5s_ease_hoverwave];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@keyframes wave {
|
|
36
|
+
0% {
|
|
37
|
+
transform: rotate(0deg);
|
|
38
|
+
}
|
|
39
|
+
2.5%,
|
|
40
|
+
7.5% {
|
|
41
|
+
transform: rotate(14deg);
|
|
42
|
+
}
|
|
43
|
+
5% {
|
|
44
|
+
transform: rotate(-8deg);
|
|
45
|
+
}
|
|
46
|
+
10% {
|
|
47
|
+
transform: rotate(-4deg);
|
|
48
|
+
}
|
|
49
|
+
12.5% {
|
|
50
|
+
transform: rotate(10deg);
|
|
51
|
+
}
|
|
52
|
+
15% {
|
|
53
|
+
transform: rotate(0deg);
|
|
54
|
+
}
|
|
55
|
+
100% {
|
|
56
|
+
transform: rotate(0deg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@keyframes hoverwave {
|
|
61
|
+
0% {
|
|
62
|
+
transform: rotate(0deg);
|
|
63
|
+
}
|
|
64
|
+
10% {
|
|
65
|
+
transform: rotate(14deg);
|
|
66
|
+
}
|
|
67
|
+
20% {
|
|
68
|
+
transform: rotate(-8deg);
|
|
69
|
+
}
|
|
70
|
+
30% {
|
|
71
|
+
transform: rotate(14deg);
|
|
72
|
+
}
|
|
73
|
+
40% {
|
|
74
|
+
transform: rotate(-4deg);
|
|
75
|
+
}
|
|
76
|
+
50% {
|
|
77
|
+
transform: rotate(10deg);
|
|
78
|
+
}
|
|
79
|
+
60% {
|
|
80
|
+
transform: rotate(0deg);
|
|
81
|
+
}
|
|
82
|
+
100% {
|
|
83
|
+
transform: rotate(0deg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
portfolio: { name },
|
|
4
|
+
} = useAppConfig();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<header class="py-0 sm:py-4">
|
|
9
|
+
<nav class="flex items-center justify-between">
|
|
10
|
+
<h2 class="text-center">
|
|
11
|
+
<a
|
|
12
|
+
href="/"
|
|
13
|
+
class="flex items-center gap-x-4 p-1 font-semibold text-xl text-zinc-950 dark:text-zinc-300 focus-visible:global-focus"
|
|
14
|
+
>
|
|
15
|
+
<img
|
|
16
|
+
src="/logo.svg"
|
|
17
|
+
alt="Logo"
|
|
18
|
+
class="size-10 rounded-xl ring ring-zinc-200 dark:ring-zinc-800 p-0.5"
|
|
19
|
+
/>
|
|
20
|
+
{{ name }}
|
|
21
|
+
</a>
|
|
22
|
+
</h2>
|
|
23
|
+
|
|
24
|
+
<theme-toggle />
|
|
25
|
+
</nav>
|
|
26
|
+
</header>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Portfolio } from "#imports";
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<Pick<Portfolio, "name" | "subtitle" | "avatar">>(), {
|
|
5
|
+
name: "Full Name",
|
|
6
|
+
subtitle: "Whatever It Is I Do",
|
|
7
|
+
});
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div
|
|
12
|
+
:style="{
|
|
13
|
+
background:
|
|
14
|
+
'#18181b radial-gradient(circle at 1px 1px, #3f3f46 1px, transparent 0) repeat 0 0 / 20px 20px',
|
|
15
|
+
}"
|
|
16
|
+
class="flex h-full w-full items-center justify-center overflow-hidden p-16 text-zinc-200 bg-zinc-900"
|
|
17
|
+
>
|
|
18
|
+
<div class="flex flex-row items-center justify-center gap-16">
|
|
19
|
+
<div class="flex flex-col items-center justify-around gap-6 text-left">
|
|
20
|
+
<h1 class="text-8xl font-black">{{ name }}</h1>
|
|
21
|
+
<p class="text-4xl font-bold">{{ subtitle }}</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- <div
|
|
25
|
+
v-if="avatar"
|
|
26
|
+
class="mx-auto w-44 h-44 shrink-0 rounded-full bg-cover bg-center p-1 ring ring-zinc-700"
|
|
27
|
+
:style="{ backgroundImage: `url(${avatar})` }"
|
|
28
|
+
/> -->
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SocialMediaKey, SocialMediaValue } from "~";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
name: SocialMediaKey;
|
|
6
|
+
icon: SocialMediaValue["socialMediaIcon"];
|
|
7
|
+
to: SocialMediaValue["profileUrl"];
|
|
8
|
+
style: {
|
|
9
|
+
backgroundColor: SocialMediaValue["socialMediaBgColor"];
|
|
10
|
+
borderColor: SocialMediaValue["socialMediaBorderColor"];
|
|
11
|
+
textColor: SocialMediaValue["socialMediaTextColor"];
|
|
12
|
+
};
|
|
13
|
+
}>();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<nuxt-link
|
|
18
|
+
:to="to"
|
|
19
|
+
:title="name"
|
|
20
|
+
:aria-label="name"
|
|
21
|
+
:class="[
|
|
22
|
+
'flex items-center justify-center text-2xl sm:text-3xl p-0! hover:scale-105! focus-visible:global-focus',
|
|
23
|
+
style.backgroundColor,
|
|
24
|
+
style.textColor,
|
|
25
|
+
style.borderColor,
|
|
26
|
+
]"
|
|
27
|
+
>
|
|
28
|
+
<Icon :name="icon" />
|
|
29
|
+
</nuxt-link>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const colorMode = useColorMode();
|
|
3
|
+
const onClick = () =>
|
|
4
|
+
colorMode.value === "light"
|
|
5
|
+
? (colorMode.preference = "dark")
|
|
6
|
+
: (colorMode.preference = "light");
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<button
|
|
11
|
+
aria-label="Color Mode"
|
|
12
|
+
class="w-10 h-10 flex items-center justify-center text-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50/50 dark:bg-zinc-800/25 hover:bg-zinc-200 dark:hover:bg-zinc-700 *:text-zinc-950 dark:*:text-zinc-300 focus-visible:global-focus"
|
|
13
|
+
@click="onClick"
|
|
14
|
+
>
|
|
15
|
+
<ColorScheme placeholder="...">
|
|
16
|
+
<template v-if="colorMode.value === 'dark'">
|
|
17
|
+
<Icon name="ph:sun-duotone" class="w-5 h-5" />
|
|
18
|
+
<span class="sr-only">Dark Mode</span>
|
|
19
|
+
</template>
|
|
20
|
+
<template v-else>
|
|
21
|
+
<Icon name="ph:moon-duotone" class="w-5 h-5" />
|
|
22
|
+
<span class="sr-only">Light Mode</span>
|
|
23
|
+
</template>
|
|
24
|
+
</ColorScheme>
|
|
25
|
+
</button>
|
|
26
|
+
</template>
|
package/app/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./utils/types";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import "@fontsource/space-grotesk/400.css";
|
|
3
|
+
import "@fontsource/space-grotesk/500.css";
|
|
4
|
+
import "@fontsource/space-grotesk/600.css";
|
|
5
|
+
import "@fontsource/space-grotesk/700.css";
|
|
6
|
+
import gsap from "gsap";
|
|
7
|
+
|
|
8
|
+
const { portfolio } = useAppConfig();
|
|
9
|
+
|
|
10
|
+
const { name, subtitle, bio } = portfolio;
|
|
11
|
+
|
|
12
|
+
const { $gsap } = useNuxtApp();
|
|
13
|
+
|
|
14
|
+
const ctx = ref<gsap.Context | null>(null);
|
|
15
|
+
|
|
16
|
+
onMounted(() => {
|
|
17
|
+
ctx.value = $gsap.context(() => {
|
|
18
|
+
$gsap.fromTo(
|
|
19
|
+
".animate-layout",
|
|
20
|
+
{ opacity: 0 },
|
|
21
|
+
{
|
|
22
|
+
opacity: 1,
|
|
23
|
+
duration: 0.75,
|
|
24
|
+
delay: 0.5,
|
|
25
|
+
ease: "elastic.out(0.5, 0.3)",
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
$gsap.fromTo(
|
|
29
|
+
".animate-element",
|
|
30
|
+
{ scale: 0.75, opacity: 0, y: 20 },
|
|
31
|
+
{
|
|
32
|
+
scale: 1,
|
|
33
|
+
opacity: 1,
|
|
34
|
+
y: 0,
|
|
35
|
+
duration: 0.15,
|
|
36
|
+
delay: 0.9,
|
|
37
|
+
ease: "elastic.out(0.5, 0.3)",
|
|
38
|
+
stagger: 0.1,
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
onUnmounted(() => {
|
|
45
|
+
ctx.value && ctx.value.revert();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
useHead({
|
|
49
|
+
bodyAttrs: {
|
|
50
|
+
class: "animate-layout opacity-0",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
useSeoMeta({
|
|
55
|
+
title: `${name} · ${subtitle}`,
|
|
56
|
+
description: bio.text,
|
|
57
|
+
ogTitle: `${name} · ${subtitle}`,
|
|
58
|
+
ogDescription: bio.text,
|
|
59
|
+
twitterCard: "summary_large_image",
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<div class="space-y-5 sm:space-y-10">
|
|
65
|
+
<app-header />
|
|
66
|
+
|
|
67
|
+
<main class="min-h-full">
|
|
68
|
+
<slot />
|
|
69
|
+
</main>
|
|
70
|
+
|
|
71
|
+
<app-footer />
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type SocialMediaKey = "X" | "LinkedIn" | "Bluesky" | "GitHub";
|
|
2
|
+
|
|
3
|
+
export interface SocialMediaValue {
|
|
4
|
+
socialMediaIcon: string;
|
|
5
|
+
socialMediaBgColor: string;
|
|
6
|
+
socialMediaBorderColor: string;
|
|
7
|
+
socialMediaTextColor: string;
|
|
8
|
+
profileUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Portfolio {
|
|
12
|
+
name: string;
|
|
13
|
+
subtitle: string;
|
|
14
|
+
avatar?: string;
|
|
15
|
+
company?: string;
|
|
16
|
+
socials: Partial<Record<SocialMediaKey, SocialMediaValue>>;
|
|
17
|
+
bio: {
|
|
18
|
+
html?: string;
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
cta: {
|
|
22
|
+
contact?: string;
|
|
23
|
+
location?: string;
|
|
24
|
+
link?: { url: string; label: string };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Style {
|
|
29
|
+
roundedItems: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare module "nuxt/schema" {
|
|
33
|
+
interface AppConfigInput {
|
|
34
|
+
portfolio: Portfolio;
|
|
35
|
+
style?: Style;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface AppConfig {
|
|
39
|
+
portfolio: Portfolio;
|
|
40
|
+
style?: Style;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
|
|
5
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
8
|
+
export default defineNuxtConfig({
|
|
9
|
+
app: {
|
|
10
|
+
rootAttrs: {
|
|
11
|
+
id: "app"
|
|
12
|
+
},
|
|
13
|
+
head: {
|
|
14
|
+
link: [{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
build: {
|
|
19
|
+
transpile: ["gsap"]
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
colorMode: { classSuffix: "" },
|
|
23
|
+
|
|
24
|
+
compatibilityDate: "2025-05-15",
|
|
25
|
+
|
|
26
|
+
css: [join(currentDir, "./app/assets/css/tailwind.css")],
|
|
27
|
+
|
|
28
|
+
alias: {
|
|
29
|
+
"@minch/bentolio/tailwind": join(
|
|
30
|
+
currentDir,
|
|
31
|
+
"./app/assets/css/tailwind.css"
|
|
32
|
+
)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
devtools: { enabled: true },
|
|
36
|
+
|
|
37
|
+
modules: [
|
|
38
|
+
"@nuxt/eslint",
|
|
39
|
+
"@nuxt/icon",
|
|
40
|
+
"@nuxtjs/color-mode",
|
|
41
|
+
"nuxt-og-image"
|
|
42
|
+
],
|
|
43
|
+
|
|
44
|
+
routeRules: {
|
|
45
|
+
"/*": { prerender: true }
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
vite: {
|
|
49
|
+
plugins: [tailwindcss()]
|
|
50
|
+
}
|
|
51
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@minch/bentolio",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "A minimal portfolio template and layer built with Nuxt 4, Tailwind CSS 4, and GSAP.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/oneminch/bentolio"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/oneminch/bentolio#readme",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Dawit",
|
|
13
|
+
"url": "https://bsky.app/profile/minch.dev"
|
|
14
|
+
},
|
|
15
|
+
"main": "./nuxt.config.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"nuxt.config.ts",
|
|
18
|
+
"app/**/*",
|
|
19
|
+
"public/**/*",
|
|
20
|
+
".env.example",
|
|
21
|
+
"pnpm-workspace.yaml",
|
|
22
|
+
"eslint.config.mjs",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"nuxt",
|
|
28
|
+
"nuxt-layer",
|
|
29
|
+
"tailwindcss",
|
|
30
|
+
"gsap"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@fontsource/space-grotesk": "^5.2.8",
|
|
38
|
+
"@iconify-json/ph": "^1.2.2",
|
|
39
|
+
"@iconify-json/simple-icons": "^1.2.47",
|
|
40
|
+
"@nuxt/eslint": "1.8.0",
|
|
41
|
+
"@nuxt/icon": "2.0.0",
|
|
42
|
+
"@nuxtjs/color-mode": "^3.5.2",
|
|
43
|
+
"@tailwindcss/vite": "^4.1.11",
|
|
44
|
+
"@unhead/vue": "^2.0.14",
|
|
45
|
+
"eslint": "^9.33.0",
|
|
46
|
+
"gsap": "^3.13.0",
|
|
47
|
+
"nuxt": "^4.0.3",
|
|
48
|
+
"nuxt-og-image": "5.1.9",
|
|
49
|
+
"tailwindcss": "^4.1.11",
|
|
50
|
+
"unstorage": "^1.16.1",
|
|
51
|
+
"vue": "^3.5.18",
|
|
52
|
+
"vue-router": "^4.5.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "nuxt build",
|
|
56
|
+
"dev": "nuxt dev",
|
|
57
|
+
"generate": "nuxt generate",
|
|
58
|
+
"preview": "nuxt preview",
|
|
59
|
+
"postinstall": "nuxt prepare"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M181 0H75C33.5786 0 0 33.5786 0 75V181C0 222.421 33.5786 256 75 256H181C222.421 256 256 222.421 256 181V75C256 33.5786 222.421 0 181 0Z" fill="#020617"/>
|
|
3
|
+
<rect x="62" y="63" width="58" height="131" rx="20" fill="#D9D9D9"/>
|
|
4
|
+
<rect x="135" y="63" width="58" height="58" rx="20" fill="#D9D9D9"/>
|
|
5
|
+
<rect x="135" y="136" width="58" height="58" rx="20" fill="#D9D9D9"/>
|
|
6
|
+
</svg>
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M181 0H75C33.5786 0 0 33.5786 0 75V181C0 222.421 33.5786 256 75 256H181C222.421 256 256 222.421 256 181V75C256 33.5786 222.421 0 181 0Z" fill="#020617"/>
|
|
3
|
+
<rect x="62" y="63" width="58" height="131" rx="20" fill="#D9D9D9"/>
|
|
4
|
+
<rect x="135" y="63" width="58" height="58" rx="20" fill="#D9D9D9"/>
|
|
5
|
+
<rect x="135" y="136" width="58" height="58" rx="20" fill="#D9D9D9"/>
|
|
6
|
+
</svg>
|
|
Binary file
|
|
Binary file
|