Compare commits
11 Commits
d14e2b139e
...
c5e02f6f0e
| Author | SHA1 | Date | |
|---|---|---|---|
| c5e02f6f0e | |||
| a9a102391e | |||
| ee04181d82 | |||
| b8a4eceab5 | |||
| 3a3ea9de09 | |||
| beaa279ffc | |||
| 30477580bd | |||
| d9f7a9df90 | |||
| b0d6f2c678 | |||
| 60ddc122c1 | |||
| 8670b689ec |
75
README.md
75
README.md
@@ -1,75 +0,0 @@
|
||||
# Nuxt 3 Minimal Starter
|
||||
|
||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm run dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm run build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm run preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
18
assets/css/main.css
Normal file
18
assets/css/main.css
Normal file
@@ -0,0 +1,18 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme static {
|
||||
--color-primary: var(--color-blue-500);
|
||||
|
||||
--color-primary-50: var(--color-blue-50);
|
||||
--color-primary-100: var(--color-blue-100);
|
||||
--color-primary-200: var(--color-blue-200);
|
||||
--color-primary-300: var(--color-blue-300);
|
||||
--color-primary-400: var(--color-blue-400);
|
||||
--color-primary-500: var(--color-blue-500);
|
||||
--color-primary-600: var(--color-blue-600);
|
||||
--color-primary-700: var(--color-blue-700);
|
||||
--color-primary-800: var(--color-blue-800);
|
||||
--color-primary-900: var(--color-blue-900);
|
||||
--color-primary-950: var(--color-blue-950);
|
||||
}
|
||||
53
components/articles/about-author.vue
Normal file
53
components/articles/about-author.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<UContainer
|
||||
class="flex flex-col md:gap-8 gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl"
|
||||
as="section"
|
||||
>
|
||||
<nuxt-img
|
||||
placeholder
|
||||
alt="Marios Antonoudiou"
|
||||
src="mariosant.webp"
|
||||
width="64"
|
||||
height="64"
|
||||
class="w-16 h-16 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300"
|
||||
/>
|
||||
|
||||
<p class="md:text-xl text-lg font-semibold">
|
||||
<ULink
|
||||
class="text-blue-500 dark:text-blue-300 hover:underline hover:underline-offset-4"
|
||||
to="mailto:mariosant@sent.com"
|
||||
>Marios Antonoudiou</ULink
|
||||
>, independent software engineer.<br />
|
||||
Building digital products that feel simple, even when they are not.
|
||||
</p>
|
||||
<p class="text-gray-800 dark:text-gray-200">
|
||||
<span class="font-semibold">Software engineer</span> specializing at
|
||||
<span class="font-semibold">Javascript & Typescript ecosystem</span>, with
|
||||
a focus on digital products, enterprise apps and streaming data.
|
||||
Well-versed in a wide range of technologies, especially on
|
||||
<span class="font-semibold">frontends</span>.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4 overflow-x-auto">
|
||||
<UButton
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="mynaui:calendar"
|
||||
target="_blank"
|
||||
to="https://cal.com/mariosant/30min"
|
||||
>
|
||||
Arrange a call
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
icon="mynaui:envelope"
|
||||
variant="soft"
|
||||
target="_blank"
|
||||
to="mailto:mariosant@sent.com"
|
||||
>
|
||||
Send a message
|
||||
</UButton>
|
||||
</div>
|
||||
</UContainer>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@ const currentYear = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer class="flex flex-col gap-5" as="footer">
|
||||
<UContainer class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl items-center justify-center" as="footer">
|
||||
<div class="text-sm text-gray-400">
|
||||
© Marios Antonoudiou {{ currentYear }} - 🏛️ Athens, Greece
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import PreviousRole from '../previous-role.vue';
|
||||
<template>
|
||||
<UContainer class="flex flex-col gap-5" as="section">
|
||||
<UContainer class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl" as="section">
|
||||
<div class="text-sm text-gray-500">Been working with</div>
|
||||
<PreviousRole
|
||||
company="Lateralus Ventures"
|
||||
logo="lateralus-logo.jpeg"
|
||||
href="https://lateralus.ventures"
|
||||
>
|
||||
<div>
|
||||
Lead the frontend development of Ask Chief, an AI-driven product,
|
||||
with a focus on building scalable, high-performance user interfaces that
|
||||
evolve alongside intelligent systems.
|
||||
</div>
|
||||
</PreviousRole>
|
||||
|
||||
<PreviousRole
|
||||
company="Celonis"
|
||||
logo="celonis-logo.webp"
|
||||
@@ -51,7 +62,7 @@ import PreviousRole from '../previous-role.vue';
|
||||
>
|
||||
<ul class="list-disc list-inside">
|
||||
<li class="inline">
|
||||
Developed the frontend delivery platform with public-facing
|
||||
Developed the customer delivery platform with public-facing
|
||||
infrastructure.
|
||||
</li>
|
||||
<li class="inline">
|
||||
@@ -59,18 +70,5 @@ import PreviousRole from '../previous-role.vue';
|
||||
</li>
|
||||
</ul>
|
||||
</PreviousRole>
|
||||
|
||||
<PreviousRole
|
||||
company="Up Hellas"
|
||||
logo="up-logo.webp"
|
||||
href="https://uphellas.gr"
|
||||
>
|
||||
<ul class="list-disc list-inside">
|
||||
<li class="inline">Implemented client's area dashboards.</li>
|
||||
<li class="inline">
|
||||
Designed and implemented internal sales dashrboard.
|
||||
</li>
|
||||
</ul>
|
||||
</PreviousRole>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<template>
|
||||
<UContainer class="flex flex-col gap-5" as="section">
|
||||
<UContainer
|
||||
class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl"
|
||||
as="section"
|
||||
>
|
||||
<div class="text-sm text-gray-500">Let's connect</div>
|
||||
|
||||
<div class="flex gap-4 overflow-x-auto">
|
||||
<UButton
|
||||
color="purple"
|
||||
icon="i-heroicons-arrow-right-circle"
|
||||
class="rounded-full"
|
||||
color="neutral"
|
||||
icon="mynaui:calendar"
|
||||
variant="soft"
|
||||
target="_blank"
|
||||
to="https://www.calendar.com/mariosant/short-video-call"
|
||||
to="https://cal.com/mariosant/30min"
|
||||
>
|
||||
Calendar
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="green"
|
||||
icon="i-heroicons-arrow-right-circle"
|
||||
class="rounded-full"
|
||||
color="neutral"
|
||||
icon="mynaui:envelope"
|
||||
variant="soft"
|
||||
target="_blank"
|
||||
to="mailto:mariosant@sent.com"
|
||||
@@ -26,9 +27,8 @@
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="orange"
|
||||
icon="i-heroicons-arrow-right-circle"
|
||||
class="rounded-full"
|
||||
color="neutral"
|
||||
icon="mynaui:brand-github"
|
||||
variant="soft"
|
||||
target="_blank"
|
||||
to="https://github.com/mariosant"
|
||||
@@ -37,9 +37,8 @@
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="blue"
|
||||
icon="i-heroicons-arrow-right-circle"
|
||||
class="rounded-full"
|
||||
color="neutral"
|
||||
icon="mynaui:brand-linkedin"
|
||||
variant="soft"
|
||||
target="_blank"
|
||||
to="https://www.linkedin.com/in/mariosant/"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<UContainer class="flex flex-col md:gap-12 gap-5" as="header">
|
||||
<UContainer
|
||||
class="flex flex-col md:gap-12 gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl"
|
||||
as="header"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<nuxt-img
|
||||
placeholder
|
||||
@@ -7,7 +10,7 @@
|
||||
src="mariosant.webp"
|
||||
width="64"
|
||||
height="64"
|
||||
class="w-16 h-16 rounded-full border dark:border-gray-800 dark:bg-slate-300"
|
||||
class="w-16 h-16 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300"
|
||||
/>
|
||||
|
||||
<div class="flex gap-3 items-center justify-end">
|
||||
@@ -31,21 +34,38 @@
|
||||
class="text-blue-500 dark:text-blue-300 hover:underline hover:underline-offset-4"
|
||||
to="mailto:mariosant@sent.com"
|
||||
>Marios Antonoudiou</ULink
|
||||
>, software engineer.<br />
|
||||
Crafting future user interfaces at
|
||||
<ULink
|
||||
to="https://celonis.com"
|
||||
target="_blank"
|
||||
class="text-green-500 dark:text-green-300 hover:underline hover:underline-offset-4"
|
||||
>Celonis</ULink
|
||||
>.
|
||||
>, independent software engineer.<br />
|
||||
Building digital products that feel simple, even when they are not.
|
||||
</h1>
|
||||
<p class="text-gray-800 dark:text-gray-200">
|
||||
<span class="font-semibold">Software engineer</span> specializing at
|
||||
<span class="font-semibold">Javascript & Typescript ecosystem</span>, with
|
||||
a focus on enterprise apps and streaming data. Well-versed in a wide range
|
||||
of technologies, especially on
|
||||
a focus on digital products, enterprise apps and streaming data.
|
||||
Well-versed in a wide range of technologies, especially on
|
||||
<span class="font-semibold">frontends</span>.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4 overflow-x-auto">
|
||||
<UButton
|
||||
size="xl"
|
||||
color="primary"
|
||||
icon="mynaui:calendar"
|
||||
target="_blank"
|
||||
to="https://cal.com/mariosant/30min"
|
||||
>
|
||||
Arrange a call
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
size="xl"
|
||||
color="neutral"
|
||||
icon="mynaui:envelope"
|
||||
variant="soft"
|
||||
target="_blank"
|
||||
to="mailto:mariosant@sent.com"
|
||||
>
|
||||
Send a message
|
||||
</UButton>
|
||||
</div>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
<template>
|
||||
<UContainer class="flex flex-col gap-5" as="section">
|
||||
<UContainer class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl" as="section">
|
||||
<div class="text-sm text-gray-500">Personal projects</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<Project
|
||||
title="Ghostwriter"
|
||||
image="https://www.ghostwriter.rocks/ghostwriter-logo.png"
|
||||
url="https://ghostwriter.rocks"
|
||||
>
|
||||
<template v-slot:description>
|
||||
Your Strava activities, creatively titled!
|
||||
</template>
|
||||
<template v-slot:subtitle> Compatible with Strava </template>
|
||||
</Project>
|
||||
|
||||
<Project
|
||||
title="Places for Zendesk"
|
||||
image="https://990141.apps.zdusercontent.com/990141/assets/1701544308-8c83d0f02afd0b3d7342e5f19304c4b4/logo.png"
|
||||
@@ -11,7 +22,9 @@
|
||||
<template v-slot:description>
|
||||
Drive leads and customers straight to your business.
|
||||
</template>
|
||||
<template v-slot:subtitle> Built for Zendesk marketplace </template>
|
||||
<template v-slot:subtitle>
|
||||
Built for Zendesk marketplace
|
||||
</template>
|
||||
</Project>
|
||||
|
||||
<Project
|
||||
@@ -23,19 +36,9 @@
|
||||
<template v-slot:description
|
||||
>Turn URLs into visual chat-friendly previews</template
|
||||
>
|
||||
<template v-slot:subtitle> Built for LiveChat marketplace </template>
|
||||
</Project>
|
||||
|
||||
<Project
|
||||
title="Currencies"
|
||||
image="https://cdn.livechat-files.com/api/file/developers/img/applications/WA1l7z4Gg/stCs0iVMg-icon-960x960.png"
|
||||
url="https://www.livechat.com/marketplace/apps/currencies"
|
||||
marketplace="LiveChat"
|
||||
>
|
||||
<template v-slot:description
|
||||
>The quickest way to convert currencies.</template
|
||||
>
|
||||
<template v-slot:subtitle> Built for LiveChat marketplace </template>
|
||||
<template v-slot:subtitle>
|
||||
Built for LiveChat marketplace
|
||||
</template>
|
||||
</Project>
|
||||
</div>
|
||||
</UContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<UContainer class="flex flex-col gap-5" as="section">
|
||||
<UContainer class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl" as="section">
|
||||
<ULink
|
||||
class="text-sm text-gray-500 flex flex-row gap-1 items-center group"
|
||||
to="/articles"
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps<Props>();
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex overflow-auto gap-x-5 md:gap-2 items-center md:items-start md:flex-col md:border md:rounded-lg md:p-4 md:dark:border-slate-600"
|
||||
class="flex overflow-auto gap-x-5 md:gap-2 items-center md:items-start md:flex-col md:border md:rounded-lg md:border-gray-200 md:p-4 md:dark:border-slate-600"
|
||||
>
|
||||
<ULink :to="props.url" target="_blank">
|
||||
<NuxtImg
|
||||
@@ -24,7 +24,7 @@ const props = defineProps<Props>();
|
||||
<div class="grid row-span-3 gap-2">
|
||||
<div>
|
||||
<ULink
|
||||
class="font-semibold hover:underline hover:underline-offset-4"
|
||||
class="font-semibold hover:underline hover:underline-offset-4 !text-slate-900 dark:text-white"
|
||||
:to="props.url"
|
||||
target="_blank"
|
||||
>{{ props.title }}</ULink
|
||||
|
||||
@@ -19,14 +19,9 @@ const motionInstance = useMotion(target, {
|
||||
|
||||
const throttledApply = useThrottleFn((state: string) => {
|
||||
return motionInstance.apply(state);
|
||||
}, 300);
|
||||
}, 500);
|
||||
|
||||
watchEffect(() => {
|
||||
if (arrivedState.top) {
|
||||
motionInstance.apply("initial");
|
||||
return;
|
||||
}
|
||||
|
||||
if (directions.top) {
|
||||
throttledApply("initial");
|
||||
return;
|
||||
@@ -42,20 +37,25 @@ watchEffect(() => {
|
||||
<template>
|
||||
<div
|
||||
ref="target"
|
||||
class="border-b dark:border-none dark:border-b-slate-600 top-0 bg-white z-10 dark:bg-slate-800 bg-opacity-85 dark:bg-opacity-90 backdrop-blur-sm sticky"
|
||||
class="border-b border-b-gray-200 dark:border-none dark:border-b-slate-600 top-0 bg-white z-10 dark:bg-slate-800 bg-opacity-85 dark:bg-opacity-90 backdrop-blur-sm sticky"
|
||||
>
|
||||
<UContainer
|
||||
class="grid grid-cols-2 items-center gap-3 !py-2 max-w-3xl"
|
||||
as="nav"
|
||||
>
|
||||
<UContainer class="grid grid-cols-2 items-center gap-3 !py-2" as="nav">
|
||||
<ULink to="/" class="flex gap-3 items-center">
|
||||
<nuxt-img
|
||||
placeholder
|
||||
alt="Marios Antonoudiou"
|
||||
src="mariosant.webp"
|
||||
class="w-10 h-10 rounded-full border dark:border-gray-800 dark:bg-slate-300 block"
|
||||
class="w-10 h-10 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
|
||||
/>
|
||||
<div class="font-bold">Marios Antonoudiou</div>
|
||||
<div class="font-bold text-slate-800 dark:text-white">
|
||||
Marios Antonoudiou
|
||||
</div>
|
||||
</ULink>
|
||||
<div
|
||||
class="flex gap-3 items-center justify-end text-slate-600 dark:text-slate-400"
|
||||
class="flex gap-4 items-center justify-end text-slate-600 dark:text-slate-400"
|
||||
>
|
||||
<ULink
|
||||
to="/"
|
||||
@@ -64,6 +64,7 @@ watchEffect(() => {
|
||||
>Home</ULink
|
||||
>
|
||||
|
||||
|
||||
<ULink
|
||||
to="/articles"
|
||||
class="font-semibold text-sm underline-offset-4"
|
||||
|
||||
39
content/articles/2025-09-15-smooth-scrolling.md
Normal file
39
content/articles/2025-09-15-smooth-scrolling.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Smooth Scrolling in Agentic Chat Interfaces
|
||||
date: 2025-09-15
|
||||
coverImage:
|
||||
author: mariosant
|
||||
url: "/agentic-ui.png"
|
||||
---
|
||||
|
||||
When building conversational UIs, small interaction details can make a huge difference in how natural the experience feels. Recently, while working on an agentic chat interface, I ran into a deceptively simple requirement:
|
||||
|
||||
Whenever the user submits a question, the viewport should scroll to that question, position it neatly at the top, and then let the user read the agent’s response at their own pace.
|
||||
|
||||
It sounds straightforward, but the implementation had a few subtle challenges.
|
||||
|
||||
## The Core Challenge
|
||||
|
||||
Most chat apps scroll new messages to the bottom, where they stack endlessly. For this interface, I wanted something more intentional:
|
||||
|
||||
* The user’s own question should appear at the top of the viewport.
|
||||
* There should be extra breathing room below it so the agent’s reply flows in naturally.
|
||||
* That extra whitespace shouldn’t stick around forever - otherwise the layout starts looking broken.
|
||||
|
||||
## Creating the Right Spacing
|
||||
|
||||
To achieve the temporary space effect, I leaned on modern CSS units like viewport height (vh) and dynamic CSS variables. The idea was simple: whenever the user sends a question, create some space at the bottom of the chat, which effectively pushes their question to the top of the screen.
|
||||
|
||||
From a user perspective, this feels intentional: their message takes center stage, while the agent’s answer appears in a comfortable, readable position just below.
|
||||
|
||||
## Knowing When to Remove It
|
||||
|
||||
The real trick was deciding when to get rid of that artificial space. Leaving it there permanently made the UI look odd, as if something was missing below the messages.
|
||||
|
||||
This is where the Intersection Observer API came into play. By watching a small invisible element placed in that whitespace, the system could detect when the user scrolled and the space was no longer visible. At that moment, the extra space was removed - restoring the chat to a normal flow.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
This kind of interaction is easy to overlook, but it shapes how people experience a conversational product. Instead of feeling like a chat feed bolted onto a webpage, the interface feels deliberate and respectful of the user’s focus.
|
||||
|
||||
By combining viewport-relative layout techniques with scroll-aware cleanup logic, I was able to create a chat UI that feels smooth, intentional, and more human.
|
||||
36
content/services/from-click-to-customer.md
Normal file
36
content/services/from-click-to-customer.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: From Click to Customer
|
||||
coverImage: https://www.livechat.com/tour/chats--hero_huff715b5b55884afe403ac6f300d757d4_241826_1856x0_resize_q75_h2_catmullrom_3.67a4d4e2301f60ac241dac3fef824ccf731078d93cb323c7d84e75d389670fa1.webp
|
||||
---
|
||||
|
||||
Most websites attract visitors but struggle to turn them into paying customers. People browse, they click, they even add to cart - but somewhere along the way, they drop off. The result? Lost sales, without clear answers.
|
||||
|
||||
**From Click to Customer** is a service that helps you uncover exactly where and why this happens.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Event Tracking Setup**
|
||||
I install PostHog across your website or ecommerce store and track every key action (page views, clicks, forms, cart events, purchases).
|
||||
2. **Journey Mapping**
|
||||
We’ll see the real paths your visitors take, where they flow, where they drop, and what stops them from buying.
|
||||
3. **Insights & Dashboards**
|
||||
I turn raw data into simple dashboards and reports, so you always know what’s working, what isn’t, and where to focus.
|
||||
|
||||
### What You Get
|
||||
|
||||
- A clear view of your customer journey
|
||||
- Pinpointed conversion bottlenecks
|
||||
- Actionable insights to increase sales
|
||||
- No more guesswork, just data-backed answers
|
||||
|
||||
### Ready to Find Out Why They Don’t Convert?
|
||||
|
||||
Every business is different. The best way to uncover why your customers aren’t buying is to take a closer look together.
|
||||
|
||||
If you’d like, we can start with a short call where I’ll:
|
||||
|
||||
- Learn about your business and your goals
|
||||
- Review your current website setup
|
||||
- Share first impressions on where conversions might be dropping
|
||||
|
||||
👉 **[Schedule a Call](#)**
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: LiveChat custom development
|
||||
coverImage: https://www.livechat.com/tour/chats--hero_huff715b5b55884afe403ac6f300d757d4_241826_1856x0_resize_q75_h2_catmullrom_3.67a4d4e2301f60ac241dac3fef824ccf731078d93cb323c7d84e75d389670fa1.webp
|
||||
---
|
||||
|
||||
Testing stands as a formidable pillar, ensuring the reliability and stability of applications. As developers, the choices we make regarding testing methodologies can significantly impact the quality of our code. In this exploration of the differences between unit and integration tests in frontend development, we emphasize that even a modest amount of testing is better than none at all.
|
||||
|
||||
## Complex Dependencies in Frontend Components
|
||||
|
||||
Frontend components often have intricate dependencies, ranging from external APIs to state management libraries. Unit testing, which aims to isolate and evaluate individual units of code in isolation, can be challenging in this context. Complex dependencies may require extensive mocking, making unit tests harder to write and maintain.
|
||||
|
||||
## Simulating Real-world Scenarios with Integration Testing
|
||||
|
||||
Integration testing steps in to address the limitations of unit testing in the frontend realm. By testing the collaboration between various components and services, integration tests provide a more holistic view of an application's behavior. This becomes crucial in simulating real-world scenarios, including slow network conditions and delayed user responses, that might not be adequately captured in unit tests.
|
||||
|
||||
## The Need for Speed: Unit Tests vs. Integration Tests
|
||||
|
||||
When it comes to speed, unit tests take the lead. The isolation of smaller units like utility functions and state management for testing means that unit tests can be executed swiftly. This quick turnaround time is crucial in a development environment where feedback loops need to be fast. For smaller units, where dependencies can be effectively managed, unit tests are the preferred choice.
|
||||
|
||||
## Running Unit Tests on a Headless Browser
|
||||
|
||||
For frontend unit tests, running them on a headless browser provides a more realistic environment compared to a Node.js simulation. A headless browser environment closely mirrors the conditions a user's browser experiences, ensuring that the tests are more representative of real-world scenarios. This approach contributes to the reliability of unit tests and enhances their value in ensuring frontend functionality.
|
||||
|
||||
## Conclusion
|
||||
|
||||
In the dynamic landscape of frontend development, the choice between unit and integration testing is not an either-or proposition but rather a strategic decision based on the specific needs of the project. While unit tests excel in speed and precision for smaller units, integration tests provide a more comprehensive evaluation of an application's behavior in complex scenarios. Striking the right balance between these testing methodologies is key to delivering a frontend that not only functions as intended but also performs robustly in diverse real-world conditions.
|
||||
@@ -5,7 +5,7 @@ useHead({
|
||||
},
|
||||
bodyAttrs: {
|
||||
class:
|
||||
"dark:bg-slate-900 selection:bg-blue-100 dark:selection:bg-slate-600",
|
||||
"dark:bg-slate-900 selection:bg-blue-100 dark:selection:bg-slate-600 text-slate-900 dark:text-white",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@ useHead({
|
||||
},
|
||||
bodyAttrs: {
|
||||
class:
|
||||
"dark:bg-slate-900 selection:bg-blue-100 dark:selection:bg-slate-600",
|
||||
"dark:bg-slate-900 selection:bg-blue-100 dark:selection:bg-slate-600 text-slate-900 dark:text-white",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,5 +17,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
css: ["~/assets/css/main.css"],
|
||||
|
||||
compatibilityDate: "2025-03-15",
|
||||
});
|
||||
|
||||
13171
package-lock.json
generated
13171
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -12,17 +12,19 @@
|
||||
"devDependencies": {
|
||||
"@nuxt/devtools": "latest",
|
||||
"@nuxt/image": "latest",
|
||||
"nuxt": "^3.16.0",
|
||||
"vue": "^3.3.8",
|
||||
"vue-router": "^4.2.5"
|
||||
"nuxt": "^4.1.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/content": "^3.3.0",
|
||||
"@nuxt/ui": "^2.11.0",
|
||||
"@nuxtjs/robots": "^3.0.0",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"@nuxt/content": "^3.7.1",
|
||||
"@nuxt/ui": "4.0.0-alpha.2",
|
||||
"@nuxtjs/robots": "^5.5.5",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"sharp": "^0.33.0"
|
||||
"@vueuse/nuxt": "^13.9.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"sharp": "^0.34.1",
|
||||
"tailwindcss": "^4.1.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@ const { data: article } = useAsyncData(path, async () => {
|
||||
<template>
|
||||
<UContainer
|
||||
v-motion-fade
|
||||
class="flex flex-col gap-3 prose dark:prose-invert"
|
||||
class="flex flex-col gap-3 prose max-w-3xl dark:prose-invert p-4 sm:p-6 lg:p-8"
|
||||
as="article"
|
||||
>
|
||||
<div class="text-sm text-slate-500">
|
||||
{{ formatDate(new Date(article.date), "Do of MMMM YYYY") }}
|
||||
</div>
|
||||
<h1>{{ article.title }}</h1>
|
||||
<h1 class="text-3xl md:text-4xl font-bold">{{ article.title }}</h1>
|
||||
</UContainer>
|
||||
|
||||
<UContainer
|
||||
class="hidden md:block"
|
||||
class="hidden md:block max-w-3xl p-4 sm:p-6 lg:p-8"
|
||||
as="figure"
|
||||
v-if="article.coverImage?.url"
|
||||
v-motion-fade
|
||||
@@ -42,9 +42,10 @@ const { data: article } = useAsyncData(path, async () => {
|
||||
class="rounded-lg"
|
||||
/>
|
||||
<ULink
|
||||
v-if="article.coverImage.authorUrl"
|
||||
:to="article.coverImage.authorUrl"
|
||||
target="_blank"
|
||||
class="text-xs text-slate-500 italic font-serif hover:underline"
|
||||
class="text-xs text-slate-500 italic font-serif hover:underline p-4 sm:p-6 lg:p-8"
|
||||
>
|
||||
Photo by {{ article.coverImage.author }}
|
||||
</ULink>
|
||||
@@ -53,12 +54,22 @@ const { data: article } = useAsyncData(path, async () => {
|
||||
<UContainer
|
||||
v-motion-fade
|
||||
:delay="500"
|
||||
class="flex flex-col gap-3 prose dark:prose-invert !pt-0"
|
||||
class="flex flex-col gap-3 prose dark:prose-invert !pt-0 max-w-3xl p-4 sm:p-6 lg:p-8"
|
||||
as="article"
|
||||
>
|
||||
<ContentRenderer :value="article" />
|
||||
<ContentRenderer :value="article" prose />
|
||||
</UContainer>
|
||||
|
||||
<UContainer
|
||||
v-motion-fade
|
||||
:delay="500"
|
||||
class="flex flex-col gap-3 prose dark:prose-invert !pt-0 max-w-3xl p-4 sm:p-6 lg:p-8"
|
||||
>
|
||||
<hr class="border border-slate-100" />
|
||||
</UContainer>
|
||||
|
||||
<ArticlesAboutAuthor v-motion-fade :delay="500" />
|
||||
|
||||
<Footer v-motion-fade :delay="500" />
|
||||
</template>
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ const articles = useAsyncData("articles", async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer v-motion-fade class="prose dark:prose-invert">
|
||||
<h1>Articles</h1>
|
||||
<UContainer v-motion-fade class="prose dark:prose-invert p-4 sm:p-6 lg:p-8 max-w-3xl">
|
||||
<h1 class="font-bold text-3xl md:text-4xl">Articles</h1>
|
||||
</UContainer>
|
||||
|
||||
<UContainer v-motion-fade :delay="500" class="grid grid-cols-12 gap-8">
|
||||
<UContainer v-motion-fade :delay="500" class="grid grid-cols-12 gap-8 max-w-3xl p-4 sm:p-6 lg:p-8" as="section">
|
||||
<UCard
|
||||
class="col-span-12 md:col-span-6"
|
||||
:ui="{ body: { padding: '' } }"
|
||||
:ui="{ body: '!p-0' }"
|
||||
v-for="article in articles.data.value"
|
||||
>
|
||||
<div class="grid gap-4 pb-4 md:pb-6">
|
||||
@@ -39,7 +39,7 @@ const articles = useAsyncData("articles", async () => {
|
||||
</div>
|
||||
|
||||
<div class="w-full px-4 md:px-6">
|
||||
<UButton variant="soft" color="gray" :to="article.path"
|
||||
<UButton variant="soft" color="neutral" :to="article.path"
|
||||
>Read article</UButton
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
useSeoMeta({
|
||||
title: "Marios Antonoudiou",
|
||||
ogTitle: "Marios Antonoudiou",
|
||||
description: "Software engineer. Crafting future user interfaces at Celonis.",
|
||||
description:
|
||||
"Independent software engineer. Building digital products that feel simple, even when they are not..",
|
||||
ogDescription:
|
||||
"Software engineer. Crafting future user interfaces at Celonis.",
|
||||
"Independent software engineer. Building digital products that feel simple, even when they are not.",
|
||||
twitterCard: "summary_large_image",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HomeIntroduction v-motion-fade />
|
||||
<HomeConnect v-motion-fade :delay="500" />
|
||||
<HomeBeenWorkingWith v-motion-fade :delay="500" />
|
||||
<HomePersonalProjects v-motion-fade :delay="500" />
|
||||
<HomeConnect v-motion-fade :delay="500" />
|
||||
<Footer v-motion-fade :delay="500" />
|
||||
</template>
|
||||
|
||||
BIN
public/agentic-ui.png
Normal file
BIN
public/agentic-ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 599 KiB |
BIN
public/lateralus-logo.jpeg
Normal file
BIN
public/lateralus-logo.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Reference in New Issue
Block a user