Migrate to astro

This commit is contained in:
2026-06-15 13:13:57 +03:00
parent 1a844c57df
commit 8c4c4af56a
81 changed files with 6200 additions and 5069 deletions

View File

@@ -0,0 +1,75 @@
---
import { Icon } from "astro-icon/components";
export type ButtonVariant = "primary" | "neutral" | "outline" | "ghost";
export type ButtonSize = "md" | "xl";
export interface Props {
href?: string;
to?: string;
target?: string;
rel?: string;
type?: "button" | "submit" | "reset";
variant?: ButtonVariant;
size?: ButtonSize;
icon?: string;
block?: boolean;
class?: string;
}
const {
href,
to,
target,
rel,
type = "button",
variant = "neutral",
size = "md",
icon,
block = false,
class: className = "",
} = Astro.props;
const VARIANT_CLASSES: Record<ButtonVariant, string> = {
primary:
"bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-300 dark:text-slate-900 dark:hover:bg-blue-200",
neutral:
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-white dark:hover:bg-slate-600",
outline:
"border border-gray-200 text-slate-900 hover:bg-gray-50 dark:border-slate-600 dark:text-white dark:hover:bg-slate-800",
ghost:
"text-slate-900 hover:bg-gray-100 dark:text-white dark:hover:bg-slate-800",
};
const SIZE_CLASSES: Record<ButtonSize, string> = {
md: "px-3 py-2 text-sm",
xl: "px-4 py-3 text-base",
};
const baseClasses = [
"inline-flex items-center justify-center gap-2 rounded-md font-semibold whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-900",
block ? "w-full" : "",
SIZE_CLASSES[size],
VARIANT_CLASSES[variant],
className,
]
.filter(Boolean)
.join(" ");
const linkProps = { href: href ?? to, target, rel };
const isLink = Boolean(href || to);
---
{
isLink ? (
<a {...linkProps} class={baseClasses}>
{icon && <Icon name={icon} class="size-5 shrink-0" />}
<slot />
</a>
) : (
<button type={type} class={baseClasses}>
{icon && <Icon name={icon} class="size-5 shrink-0" />}
<slot />
</button>
)
}

View File

@@ -1,127 +0,0 @@
---
import Button from "./ui/button.astro";
---
<!-- To make this contact form work, create your free access key from https://web3forms.com/
Then you will get all form submissions in your email inbox. -->
<form
action="https://api.web3forms.com/submit"
method="POST"
id="form"
class="needs-validation"
novalidate>
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY_HERE" />
<!-- Create your free access key from https://web3forms.com/ -->
<input type="checkbox" class="hidden" style="display:none" name="botcheck" />
<div class="mb-5">
<input
type="text"
placeholder="Full Name"
required
class="w-full px-4 py-3 border-2 placeholder:text-gray-800 rounded-md outline-hidden focus:ring-4 border-gray-300 focus:border-gray-600 ring-gray-100"
name="name"
/>
<div class="empty-feedback invalid-feedback text-red-400 text-sm mt-1">
Please provide your full name.
</div>
</div>
<div class="mb-5">
<label for="email_address" class="sr-only">Email Address</label><input
id="email_address"
type="email"
placeholder="Email Address"
name="email"
required
class="w-full px-4 py-3 border-2 placeholder:text-gray-800 rounded-md outline-hidden focus:ring-4 border-gray-300 focus:border-gray-600 ring-gray-100"
/>
<div class="empty-feedback text-red-400 text-sm mt-1">
Please provide your email address.
</div>
<div class="invalid-feedback text-red-400 text-sm mt-1">
Please provide a valid email address.
</div>
</div>
<div class="mb-3">
<textarea
name="message"
required
placeholder="Your Message"
class="w-full px-4 py-3 border-2 placeholder:text-gray-800 rounded-md outline-hidden h-36 focus:ring-4 border-gray-300 focus:border-gray-600 ring-gray-100"
></textarea>
<div class="empty-feedback invalid-feedback text-red-400 text-sm mt-1">
Please enter your message.
</div>
</div>
<Button type="submit" size="lg" block>Send Message</Button>
<div id="result" class="mt-3 text-center"></div>
</form>
<style>
.invalid-feedback,
.empty-feedback {
display: none;
}
.was-validated :placeholder-shown:invalid ~ .empty-feedback {
display: block;
}
.was-validated :not(:placeholder-shown):invalid ~ .invalid-feedback {
display: block;
}
.is-invalid,
.was-validated :invalid {
border-color: #dc3545;
}
</style>
<script is:inline>
const form = document.getElementById("form");
const result = document.getElementById("result");
form.addEventListener("submit", function (e) {
e.preventDefault();
form.classList.add("was-validated");
if (!form.checkValidity()) {
form.querySelectorAll(":invalid")[0].focus();
return;
}
const formData = new FormData(form);
const object = Object.fromEntries(formData);
const json = JSON.stringify(object);
result.innerHTML = "Sending...";
fetch("https://api.web3forms.com/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: json,
})
.then(async (response) => {
let json = await response.json();
if (response.status == 200) {
result.classList.add("text-green-500");
result.innerHTML = json.message;
} else {
console.log(response);
result.classList.add("text-red-500");
result.innerHTML = json.message;
}
})
.catch((error) => {
console.log(error);
result.innerHTML = "Something went wrong!";
})
.then(function () {
form.reset();
form.classList.remove("was-validated");
setTimeout(() => {
result.style.display = "none";
}, 5000);
});
});
</script>

View File

@@ -1,7 +1,16 @@
---
const { class: className } = Astro.props;
type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12;
export interface Props {
as?: string;
class?: string;
gap?: Gap;
}
const { as: Tag = "div", class: className = "", gap = 0 } = Astro.props;
const gapClass = gap === 0 ? "" : `gap-${gap}`;
---
<div class:list={["max-w-(--breakpoint-xl) mx-auto px-5", className]}>
<Tag class:list={["flex flex-col p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full", gapClass, className]}>
<slot />
</div>
</Tag>

View File

@@ -1,17 +0,0 @@
---
import Link from "./ui/link.astro";
---
<div
class="bg-black p-8 md:px-20 md:py-20 mt-20 mx-auto max-w-5xl rounded-lg flex flex-col items-center text-center">
<h2 class="text-white text-4xl md:text-6xl tracking-tight">
Build faster websites.
</h2>
<p class="text-slate-400 mt-4 text-lg md:text-xl">
Pull content from anywhere and serve it fast with Astro's next-gen island
architecture.
</p>
<div class="flex mt-5">
<Link href="#" style="inverted">Get Started</Link>
</div>
</div>

View File

@@ -1,69 +0,0 @@
---
// @ts-ignore
import { Icon } from "astro-icon/components";
const features = [
{
title: "Bring Your Own Framework",
description:
"Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.",
icon: "bx:bxs-briefcase",
},
{
title: "100% Static HTML, No JS",
description:
"Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.",
icon: "bx:bxs-window-alt",
},
{
title: "On-Demand Components",
description:
"Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. ",
icon: "bx:bxs-data",
},
{
title: "Broad Integration",
description:
"Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any other npm packages.",
icon: "bx:bxs-bot",
},
{
title: "SEO Enabled",
description:
"Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication. It just works!",
icon: "bx:bxs-file-find",
},
{
title: "Community",
description:
"Astro is an open source project powered by hundreds of contributors making thousands of individual contributions.",
icon: "bx:bxs-user",
},
];
---
<div class="mt-16 md:mt-0">
<h2 class="text-4xl lg:text-5xl font-bold lg:tracking-tight">
Everything you need to start a website
</h2>
<p class="text-lg mt-4 text-slate-600">
Astro comes batteries included. It takes the best parts of state-of-the-art
tools and adds its own innovations.
</p>
</div>
<div class="grid sm:grid-cols-2 md:grid-cols-3 mt-16 gap-16">
{
features.map((item) => (
<div class="flex gap-4 items-start">
<div class="mt-1 bg-black rounded-full p-2 w-8 h-8 shrink-0">
<Icon class="text-white" name={item.icon} />
</div>
<div>
<h3 class="font-semibold text-lg">{item.title}</h3>{" "}
<p class="text-slate-500 mt-2 leading-relaxed">{item.description}</p>
</div>
</div>
))
}
</div>

View File

@@ -1,19 +1,9 @@
<footer class="my-20">
<p class="text-center text-sm text-slate-500">
Copyright © {new Date().getFullYear()} Astroship. All rights reserved.
</p>
<!--
Can we ask you a favor 🙏
Please keep this backlink on your website if possible.
or Purchase a commercial license from https://web3templates.com
-->
<p class="text-center text-xs text-slate-500 mt-1">
Made by <a
href="https://web3templates.com"
target="_blank"
rel="noopener"
class="hover:underline">
Web3Templates
</a>
</p>
---
const currentYear = new Date().getFullYear();
---
<footer class="flex flex-col gap-2 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full items-center justify-center">
<div class="text-sm text-gray-400">
© Marios Antonoudiou {currentYear} - 🏛️ Athens, Greece
</div>
</footer>

View File

@@ -1,53 +0,0 @@
---
import { Picture } from "astro:assets";
import heroImage from "@/assets/hero.png";
import Link from "@/components/ui/link.astro";
import { Icon } from "astro-icon/components";
---
<main
class="grid lg:grid-cols-2 place-items-center pt-16 pb-8 md:pt-12 md:pb-24">
<div class="py-6 md:order-1 hidden md:block">
<Picture
src={heroImage}
alt="Astronaut in the air"
widths={[200, 400, 600]}
sizes="(max-width: 800px) 100vw, 620px"
loading="eager"
format="avif"
/>
</div>
<div>
<h1
class="text-5xl lg:text-6xl xl:text-7xl font-bold lg:tracking-tight xl:tracking-tighter">
Marketing website done with Astro
</h1>
<p class="text-lg mt-4 text-slate-600 max-w-xl">
Astroship is a starter template for startups, marketing websites & landing
pages.<wbr /> Built with Astro.build and TailwindCSS. You can quickly create
any website with this starter.
</p>
<div class="mt-6 flex flex-col sm:flex-row gap-3">
<Link
href="#"
href="https://web3templates.com/templates/astroship-starter-website-template-for-astro"
target="_blank"
class="flex gap-1 items-center justify-center"
rel="noopener">
<Icon class="text-white w-5 h-5" name="bx:bxs-cloud-download" />
Download for Free
</Link>
<Link
size="lg"
style="outline"
rel="noopener"
href="https://github.com/surjithctly/astroship"
class="flex gap-1 items-center justify-center"
target="_blank">
<Icon class="text-black w-4 h-4" name="bx:bxl-github" />
GitHub Repo
</Link>
</div>
</div>
</main>

View File

@@ -0,0 +1,56 @@
---
import PreviousRole from "@/components/previous-role.astro";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="text-sm text-slate-500 dark:text-slate-400">Been building with</div>
<PreviousRole
company="Lateralus Ventures"
logo="/lateralus-logo.jpeg"
href="https://lateralus.ventures"
>
Led frontend development for Ask Chief, shaping AI product experiences with scalable interfaces
that could evolve alongside intelligent workflows.
</PreviousRole>
<PreviousRole
company="Celonis"
logo="/celonis-logo.webp"
href="https://celonis.com"
>
Built analytical product interfaces for OCPM, alongside shared component systems, build pipelines,
and platform foundations that helped teams move faster together.
</PreviousRole>
<PreviousRole
company="Lenses.io"
logo="/lenses-logo.webp"
href="https://lenses.io"
>
<ul class="list-disc list-inside space-y-1">
<li>Engineered product interfaces for SQL Processors and Data Catalog.</li>
<li>Architected the shared components library.</li>
<li>Worked on developer tooling, including the CLI.</li>
</ul>
</PreviousRole>
<PreviousRole company="The Chat Shop" logo="/thechatshop-logo.jpg">
<ul class="list-disc list-inside space-y-1">
<li>Spearheaded end-to-end processing architecture.</li>
<li>Established the company's streaming data infrastructure.</li>
<li>Built the operational foundations for reliable delivery and automation.</li>
</ul>
</PreviousRole>
<PreviousRole
company="Commversion"
logo="/commversion-logo.webp"
href="https://commversion.com"
>
<ul class="list-disc list-inside space-y-1">
<li>Developed the customer delivery platform and its public-facing infrastructure.</li>
<li>Designed and implemented the initial InstantConnect system.</li>
</ul>
</PreviousRole>
</section>

View File

@@ -0,0 +1,50 @@
---
import Button from "@/components/button.astro";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="text-sm text-slate-500 dark:text-slate-400">Let's connect</div>
<div class="flex gap-4 overflow-x-auto">
<Button
variant="neutral"
icon="mynaui:calendar"
href="https://cal.com/mariosant/30min"
target="_blank"
rel="noopener noreferrer">
Calendar
</Button>
<Button
variant="neutral"
icon="mynaui:envelope"
href="mailto:mariosant@sent.com"
target="_blank"
rel="noopener noreferrer">
Email
</Button>
<Button
variant="neutral"
icon="mynaui:brand-github"
href="https://github.com/mariosant"
target="_blank"
rel="noopener noreferrer">
Github
</Button>
<Button
variant="neutral"
icon="mynaui:brand-linkedin"
href="https://www.linkedin.com/in/mariosant/"
target="_blank"
rel="noopener noreferrer">
Linkedin
</Button>
<Button
variant="neutral"
icon="hugeicons:bluesky"
href="https://bsky.app/profile/mariosant.bsky.social"
target="_blank"
rel="noopener noreferrer">
Bluesky
</Button>
</div>
</section>

View File

@@ -0,0 +1,71 @@
---
import Button from "@/components/button.astro";
import Link from "@/components/link.astro";
---
<header class="flex flex-col md:gap-12 gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="flex items-center justify-between">
<a href="/" aria-label="Home">
<img
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 block"
loading="eager"
/>
</a>
<div class="flex gap-3 items-center justify-end">
<Link
href="/"
variant="default"
class="text-slate-600 dark:text-slate-400 font-semibold"
activeClass="text-slate-800 dark:text-slate-200 underline"
isActive={Astro.url.pathname === "/"}
>
Home
</Link>
<Link
href="/articles"
variant="default"
class="text-slate-600 dark:text-slate-400 font-semibold"
activeClass="text-slate-800 dark:text-slate-200 underline"
isActive={Astro.url.pathname.startsWith("/articles")}
>
Articles
</Link>
</div>
</div>
<h1 class="md:text-xl text-lg font-semibold">
<Link href="mailto:mariosant@sent.com" variant="primary">Marios Antonoudiou</Link>, AI product engineer.<br />
Building AI-powered products that feel simple, useful, and ready for real users.
</h1>
<p class="text-slate-800 dark:text-slate-200">
I design and ship <span class="font-semibold">AI-enabled product experiences</span> across
frontend architecture, interaction design, and product workflows. My focus is turning complex
systems, data, and model capabilities into software people can actually
<span class="font-semibold">understand, trust, and use</span>.
</p>
<div class="flex gap-4 overflow-x-auto">
<Button
size="xl"
variant="primary"
icon="mynaui:calendar"
href="https://cal.com/mariosant/30min"
target="_blank"
rel="noopener noreferrer"
>
Arrange a call
</Button>
<Button
size="xl"
variant="neutral"
icon="mynaui:envelope"
href="mailto:mariosant@sent.com"
target="_blank"
rel="noopener noreferrer"
>
Send a message
</Button>
</div>
</header>

View File

@@ -0,0 +1,56 @@
---
import { getCollection } from "astro:content";
import { formatDoMMMMYYYY } from "@/utils/date";
const articles = (await getCollection("articles")).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
const article = articles[0];
if (!article) {
throw new Error("No articles found in the 'articles' content collection.");
}
const cover = article.data.coverImage;
const coverUrl = cover.url.replace("w=1287&h=600", "w=300&h=300");
const body = (article.body ?? "").trim();
const excerpt = body.length > 150 ? `${body.slice(0, 150)}…` : body;
---
<section class="p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<span class="text-sm text-slate-500 dark:text-slate-400 flex flex-row gap-1 items-center">
Latest article
</span>
<a
href={`/articles/${article.id}`}
class="block mt-4 p-4 rounded-lg border border-gray-200 dark:border-slate-600 hover:border-gray-300 dark:hover:border-slate-500 transition-colors"
>
<div class="flex gap-6 items-start flex-row-reverse">
{
cover.url && (
<img
class="w-24 h-24 object-cover rounded-lg shrink-0"
src={coverUrl}
alt={article.data.title}
loading="lazy"
/>
)
}
<div class="flex flex-col gap-2 min-w-0">
<h3 class="font-semibold text-lg text-slate-900 dark:text-white">
{article.data.title}
</h3>
{excerpt && (
<p class="text-sm text-slate-600 dark:text-slate-400 line-clamp-3">
{excerpt}
</p>
)}
<time class="text-xs text-slate-500 dark:text-slate-400">
{formatDoMMMMYYYY(article.data.date)}
</time>
</div>
</div>
</a>
</section>

View File

@@ -0,0 +1,42 @@
---
import Project from "@/components/project.astro";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="text-sm text-slate-500 dark:text-slate-400">Personal projects</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
<Project
title="Ghostwriter"
image="https://app.ghostwriter.rocks/_ipx/_/ghostwriter-logo.png"
url="https://ghostwriter.rocks"
>
<Fragment slot="description">
Creative post-generation for Strava activities with a lightweight, playful product feel.
</Fragment>
<Fragment slot="subtitle">Compatible with Strava</Fragment>
</Project>
<Project
title="Places for Zendesk"
image="https://990141.apps.zdusercontent.com/990141/assets/1701544308-8c83d0f02afd0b3d7342e5f19304c4b4/logo.png"
url="https://getplaces.co"
>
<Fragment slot="description">
Location-aware lead capture designed to connect digital conversations to real-world businesses.
</Fragment>
<Fragment slot="subtitle">Built for Zendesk marketplace</Fragment>
</Project>
<Project
title="SneakPeek"
image="https://cdn.livechat-files.com/api/file/developers/img/applications/449B6QFGg/icons/AecAyk1Gg-960x960.png"
url="https://www.livechat.com/marketplace/apps/sneakpeek"
>
<Fragment slot="description">
Turn plain links into richer, more informative chat experiences.
</Fragment>
<Fragment slot="subtitle">Built for LiveChat marketplace</Fragment>
</Project>
</div>
</section>

45
src/components/link.astro Normal file
View File

@@ -0,0 +1,45 @@
---
export type LinkVariant = "default" | "primary" | "muted";
export interface Props {
href: string;
class?: string;
variant?: LinkVariant;
activeClass?: string;
isActive?: boolean;
target?: string;
rel?: string;
}
const {
href,
class: className = "",
variant = "default",
activeClass,
isActive = false,
target,
rel,
} = Astro.props;
const VARIANT_CLASSES: Record<LinkVariant, string> = {
default:
"text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
primary:
"text-blue-500 dark:text-blue-300 hover:underline hover:underline-offset-4",
muted:
"text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white",
};
const baseClasses = [
"underline-offset-4 transition-colors",
isActive && activeClass ? activeClass : "",
VARIANT_CLASSES[variant],
className,
]
.filter(Boolean)
.join(" ");
---
<a href={href} class={baseClasses} target={target} rel={rel}>
<slot />
</a>

View File

@@ -1,18 +0,0 @@
---
// @ts-ignore
import { Icon } from "astro-icon/components";
---
<div class="mt-24">
<h2 class="text-center text-slate-500">Works with your technologies</h2>
<div class="flex gap-8 md:gap-20 items-center justify-center mt-10 flex-wrap">
<Icon class="size-8 md:size-12" name="simple-icons:react" />
<Icon class="size-8 md:size-12" name="simple-icons:svelte" />
<Icon class="size-8 md:size-12" name="simple-icons:astro" />
<Icon class="size-8 md:size-16" name="simple-icons:tailwindcss" />
<Icon class="size-8 md:size-16" name="simple-icons:alpinedotjs" />
<Icon class="size-8 md:size-12" name="simple-icons:vercel" />
</div>
</div>

View File

@@ -1,47 +0,0 @@
---
import { Dropdown as DropdownContainer, DropdownItems } from "astro-navbar";
const { title, lastItem, children } = Astro.props;
---
<li class="relative">
<DropdownContainer class="group">
<button
class="flex items-center gap-1 w-full lg:w-auto lg:px-3 py-2 text-gray-600 hover:text-gray-900">
<span>{title}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 mt-0.5 group-open:rotate-180">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>
</svg>
</button>
<DropdownItems>
<div
class:list={[
"lg:absolute w-full lg:w-48 z-10",
lastItem
? "lg:right-0 origin-top-right"
: "lg:left-0 origin-top-left",
]}>
<div
class="px-3 lg:py-2 lg:bg-white lg:rounded-md lg:shadow-sm lg:border flex flex-col">
{
children.map((item) => (
<a
href={item.path}
class="py-1 text-gray-600 hover:text-gray-900">
{item.title}
</a>
))
}
</div>
</div>
</DropdownItems>
</DropdownContainer>
</li>

View File

@@ -1,99 +0,0 @@
---
import Container from "@/components/container.astro";
import Link from "@/components/ui/link.astro";
import Dropdown from "./dropdown.astro";
import { Astronav, MenuItems, MenuIcon } from "astro-navbar";
const menuitems = [
{
title: "Features",
path: "#",
children: [
{ title: "Action", path: "/" },
{ title: "Another action", path: "#" },
{ title: "Dropdown Submenu", path: "#" },
{ title: "404 Page", path: "/404" },
],
},
{
title: "Pricing",
path: "/pricing",
},
{
title: "About",
path: "/about",
},
{
title: "Blog",
path: "/blog",
},
{
title: "Contact",
path: "/contact",
},
{
title: "Pro Version",
badge: true,
path: "https://astroship-pro.web3templates.com/",
},
];
---
<Container>
<header class="flex flex-col lg:flex-row justify-between items-center my-5">
<Astronav>
<div class="flex w-full lg:w-auto items-center justify-between">
<a href="/" class="text-lg"
><span class="font-bold text-slate-800">Astro</span><span
class="text-slate-500">ship</span
>
</a>
<div class="block lg:hidden">
<MenuIcon class="w-4 h-4 text-gray-800" />
</div>
</div>
<MenuItems class="hidden w-full lg:w-auto mt-2 lg:flex lg:mt-0">
<ul class="flex flex-col lg:flex-row lg:gap-3">
{
menuitems.map((item, index) => (
<>
{item.children && (
<Dropdown
title={item.title}
children={item.children}
lastItem={index === menuitems.length - 1}
/>
)}
{!item.children && (
<li>
<a
href={item.path}
class="flex lg:px-3 py-2 items-center text-gray-600 hover:text-gray-900">
<span> {item.title}</span>
{item.badge && (
<span class="ml-1 px-2 py-0.5 text-[10px] animate-pulse font-semibold uppercase text-white bg-indigo-600 rounded-full">
New
</span>
)}
</a>
</li>
)}
</>
))
}
</ul>
<div class="lg:hidden flex items-center mt-3 gap-4">
<Link href="#" style="muted" block size="md">Log in</Link>
<Link href="#" size="md" block>Sign up</Link>
</div>
</MenuItems>
</Astronav>
<div>
<div class="hidden lg:flex items-center gap-4">
<a href="#">Log in</a>
<Link href="#" size="md">Sign up</Link>
</div>
</div>
</header>
</Container>

View File

@@ -0,0 +1,52 @@
---
export interface Props {
logo: string;
href?: string;
company: string;
}
const { logo, href, company } = Astro.props;
---
<div class="flex gap-5">
{
href ? (
<a href={href} target="_blank" rel="noopener noreferrer" class="block shrink-0">
<img
src={logo}
alt={company}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white"
loading="lazy"
/>
</a>
) : (
<img
src={logo}
alt={company}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white shrink-0"
loading="lazy"
/>
)
}
<div class="flex flex-col justify-center gap-1">
{
href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
class="md:text-xl text-lg font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white"
>
{company}
</a>
) : (
<span class="md:text-xl text-lg font-semibold text-slate-900 dark:text-white">
{company}
</span>
)
}
<div class="text-sm text-slate-500 dark:text-slate-400 lg:max-w-lg">
<slot />
</div>
</div>
</div>

View File

@@ -1,45 +0,0 @@
---
import { Tick } from "@/components/ui/icons";
import Link from "@/components/ui/link.astro";
const { plan } = Astro.props;
---
<div>
<div
class="flex flex-col w-full order-first lg:order-none border-2 border-[#D8DEE9] border-opacity-50 py-5 px-6 rounded-md">
<div class="text-center">
<h4 class="text-lg font-medium text-gray-400">{plan.name}</h4><p
class="mt-3 text-4xl font-bold text-black md:text-4xl">
{
plan.price && typeof plan.price === "object"
? plan.price.monthly
: plan.price
}
</p>
<!-- {
plan.price.original && (
<p class="mt-1 text-xl font-medium text-gray-400 line-through md:text-2xl">
{plan.price.original}
</p>
)
} -->
</div><ul class="grid mt-8 text-left gap-y-4">
{
plan.features.map((item) => (
<li class="flex items-start gap-3 text-gray-800">
<Tick class="w-6 h-6" />
<span>{item}</span>
</li>
))
}
</ul><div class="flex mt-8">
<Link
href={plan.button.link || "#"}
block
style={plan.popular ? "primary" : "outline"}>
{plan.button.text || "Get Started"}
</Link>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
---
export interface Props {
title: string;
image: string;
url: string;
}
const { title, image, url } = Astro.props;
---
<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:border-gray-200 md:p-4 md:dark:border-slate-600">
<a href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
<img
src={image}
alt={title}
class="block min-w-[50px] w-[50px] md:w-16 h-[50px] md:h-16 object-contain bg-white rounded"
loading="lazy"
/>
</a>
<div class="grid row-span-3 gap-2 min-w-0">
<div>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white"
>
{title}
</a>
<div class="text-sm text-slate-500 dark:text-slate-400 md:text-xs">
<slot name="subtitle" />
</div>
</div>
<div class="text-sm text-slate-600 dark:text-slate-400 hidden md:block">
<slot name="description">Lorem ipsum dorcet sit amet</slot>
</div>
</div>
</div>

View File

@@ -1,12 +0,0 @@
---
const { align = "center" } = Astro.props;
---
<div class:list={["mt-16", align === "center" && "text-center"]}>
<h1 class="text-4xl lg:text-5xl font-bold lg:tracking-tight">
<slot name="title">Title</slot>
</h1>
<p class="text-lg mt-4 text-slate-600">
<slot name="desc">Some description goes here</slot>
</p>
</div>

View File

@@ -0,0 +1,45 @@
---
import Link from "@/components/link.astro";
const path = Astro.url.pathname;
const isHome = path === "/" || path === "";
const isArticles = path === "/articles" || path.startsWith("/articles/");
---
<div class="sticky top-0 z-10 border-b border-gray-200 dark:border-slate-600 bg-white/85 dark:bg-slate-800/90 backdrop-blur-sm">
<nav class="grid grid-cols-2 items-center gap-3 !py-2 max-w-3xl mx-auto w-full p-4 sm:p-6 lg:p-8">
<Link href="/" class="flex gap-3 items-center" variant="default">
<img
alt="Marios Antonoudiou"
src="/mariosant.webp"
width="40"
height="40"
class="w-10 h-10 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager"
/>
<div class="font-bold text-slate-800 dark:text-white">
Marios Antonoudiou
</div>
</Link>
<div class="flex gap-4 items-center justify-end text-slate-600 dark:text-slate-400">
<Link
href="/"
variant="default"
class="font-semibold text-sm"
activeClass="text-slate-900 dark:text-slate-200 underline"
isActive={isHome}
>
Home
</Link>
<Link
href="/articles"
variant="default"
class="font-semibold text-sm"
activeClass="text-slate-900 dark:text-slate-200 underline"
isActive={isArticles}
>
Articles
</Link>
</div>
</nav>
</div>

View File

@@ -1,40 +0,0 @@
---
interface Props {
size?: "md" | "lg";
block?: boolean;
style?: "outline" | "primary" | "inverted";
class?: string;
[x: string]: any;
}
const {
size = "md",
style = "primary",
block,
class: className,
...rest
} = Astro.props;
const sizes = {
md: "px-5 py-2.5",
lg: "px-6 py-3",
};
const styles = {
outline: "border-2 border-black hover:bg-black text-black hover:text-white",
primary:
"bg-black text-white hover:bg-slate-900 border-2 border-transparent",
};
---
<button
{...rest}
class:list={[
"rounded-sm text-center transition focus-visible:ring-2 ring-offset-2 ring-gray-200",
block && "w-full",
sizes[size],
styles[style],
className,
]}
><slot />
</button>

View File

@@ -1 +0,0 @@
export { default as Tick } from "./tick.astro";

View File

@@ -1,16 +0,0 @@
---
const { class: className } = Astro.props;
---
<svg
class={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
><circle cx="12" cy="12" r="9" fill="currentColor" fill-opacity=".16"
></circle><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.75 12a8.25 8.25 0 0 1 11.916-7.393.75.75 0 1 0 .668-1.343A9.713 9.713 0 0 0 12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75c0-.366-.02-.727-.06-1.082a.75.75 0 1 0-1.49.164A8.25 8.25 0 1 1 3.75 12Zm17.78-6.47a.75.75 0 0 0-1.06-1.06L12 12.94l-2.47-2.47a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l9-9Z"
fill="currentColor"></path>
</svg>

View File

@@ -1,44 +0,0 @@
---
interface Props {
href: string;
size?: "md" | "lg";
block?: boolean;
style?: "outline" | "primary" | "inverted" | "muted";
class?: string;
[x: string]: any;
}
const {
href,
block,
size = "lg",
style = "primary",
class: className,
...rest
} = Astro.props;
const sizes = {
lg: "px-5 py-2.5",
md: "px-4 py-2",
};
const styles = {
outline: "bg-white border-2 border-black hover:bg-gray-100 text-black ",
primary: "bg-black text-white hover:bg-gray-800 border-2 border-transparent",
inverted: "bg-white text-black border-2 border-transparent",
muted: "bg-gray-100 hover:bg-gray-200 border-2 border-transparent",
};
---
<a
href={href}
{...rest}
class:list={[
"rounded-sm text-center transition focus-visible:ring-2 ring-offset-2 ring-gray-200",
block && "w-full",
sizes[size],
styles[style],
className,
]}
><slot />
</a>