Initial commit

This commit is contained in:
Martin Berg Alstad
2024-09-25 22:20:47 +02:00
parent bb58f603b2
commit 83f018c3b8
60 changed files with 6301 additions and 6615 deletions

View File

@ -1,61 +1,61 @@
---
interface Props {
title: string;
body: string;
href: string;
title: string
body: string
href: string
}
const { href, title, body } = Astro.props;
const { href, title, body } = Astro.props
---
<li class="link-card">
<a href={href}>
<h2>
{title}
<span>&rarr;</span>
</h2>
<p>
{body}
</p>
</a>
<a href={href}>
<h2>
{title}
<span>&rarr;</span>
</h2>
<p>
{body}
</p>
</a>
</li>
<style>
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card > a {
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card > a {
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
</style>

View File

@ -0,0 +1,37 @@
---
// TODO form
// TODO self-host email server
import "../styles/global.css"
import Input from "../components/Input.astro"
import * as console from "node:console"
if (Astro.request.method === "POST") {
try {
const data = await Astro.request.formData()
const name = data.get("name")
const subject = data.get("subject")
const email = data.get("email")
const message = data.get("message")
// TODO Do something with the data
console.info({ name, subject, email, message })
} catch (error) {
if (error instanceof Error) {
console.error(error.message)
}
}
}
---
<div class="text-red-600 text-center">In development</div>
<form class="flex flex-col gap-2 max-w-[500px] mx-auto" method="post">
<Input label="Name" type="text" name="name" required />
<Input label="Subject" name="subject" required />
<Input label="Email" name="email" />
<label class="flex flex-col"
>Message
<textarea name="message" class="textarea textarea-bordered" required
></textarea>
</label>
<button type="submit">Send</button>
</form>

View File

@ -0,0 +1,9 @@
---
import GiteaLink from "./links/GiteaLink.astro"
const gitUrl = import.meta.env.GIT_URL
---
<div class="divider"></div>
<div class="mx-auto">
<GiteaLink href={gitUrl} />
</div>

View File

@ -0,0 +1,20 @@
---
import { Image } from "astro:assets"
import me from "../images/me.jpg"
import * as m from "../paraglide/messages.js"
import "../styles/global.css"
---
<div class="flex items-center justify-around">
<div class="m-5">
<h1 class="text-7xl font-bold">
{m.hiIm()}
<br />
Martin Berg Alstad
<br />
{m.position()}
</h1>
<p class="mx-1 my-10">{m.aboutMe()}</p>
</div>
<Image src={me} alt="Me on a hike" width="400" height="400" class="m-5" />
</div>

View File

@ -0,0 +1,11 @@
<script lang="ts">
import Select from "./Select.svelte"
function onChange({ detail }: CustomEvent<string>) {
console.log(detail)
}
// TODO show the selected hardware
</script>
<Select options={["CPU", "GPU", "RAM"]} on:change={onChange} />

View File

@ -0,0 +1,28 @@
---
interface Props {
label: string
type?: "text" | "email" | "password" | "number"
name: string
required?: boolean
placeholder?: string
}
const {
label,
type = "text",
name,
required = false,
placeholder,
} = Astro.props
---
<label class="flex flex-col">
{label}
<input
class="input input-bordered"
type={type}
name={name}
required={required}
placeholder={placeholder}
/>
</label>

View File

@ -0,0 +1,13 @@
---
import Links from "../links"
---
<div class="flex justify-end">
{
Links.map(({ to, label }) => (
<a href={to} class="m-2 hover:underline">
{label}
</a>
))
}
</div>

View File

@ -0,0 +1,32 @@
---
import { Image } from "astro:assets"
import { type ImageMetadata } from "astro"
import BadgeList from "./badge/BadgeList.astro"
interface Props {
title: string
description: string
tags: string[]
image: ImageMetadata
imageAlt: string
linkTo: string
}
const { title, description, tags, image, imageAlt, linkTo } = Astro.props
---
<a
href={linkTo}
class="card bg-base-100 w-96 shadow-xl hover:scale-105 transition"
>
<figure>
<Image src={image} alt={imageAlt} />
</figure>
<div class="card-body">
<h2 class="card-title">
{title}
</h2>
<p>{description}</p>
<BadgeList tags={tags} />
</div>
</a>

View File

@ -0,0 +1,55 @@
---
import Layout from "../layouts/Layout.astro"
import { Image } from "astro:assets"
import { getEntry } from "astro:content"
import BadgeList from "./badge/BadgeList.astro"
import ExternalLink from "./links/ExternalLink.astro"
import * as m from "../paraglide/messages"
import { languageTag } from "../paraglide/runtime"
import Gitea from "../icons/Gitea.astro"
import "../styles/global.css"
import GiteaLink from "./links/GiteaLink.astro"
interface Props {
project: string // TODO typeof project slug
}
const { project } = Astro.props
const entry = await getEntry("projects", project)
const { Content } = await entry!.render()
const {
title,
description,
tags,
heroImage,
heroImageAlt,
source,
createdAt,
updatedAt
} = entry!.data
---
<!--TODO day.js for dates?-->
<Layout title={title} class="mx-auto max-w-[750px]">
<div class="flex justify-between my-2">
<div>
<h1>{title}</h1>
<BadgeList tags={tags} />
</div>
<div class="flex flex-col items-end">
<p>
{m.createdAt()}: {new Date(createdAt).toLocaleDateString(languageTag())}
</p>
<p>
{m.updatedAt()}: {new Date(updatedAt).toLocaleDateString(languageTag())}
</p>
</div>
</div>
<Image src={heroImage} alt={heroImageAlt} />
<GiteaLink href={source} />
<p class="my-2">{description}</p>
<Content />
</Layout>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
export let options: string[] = []
const dispatch = createEventDispatcher<{ change: string }>()
</script>
<select class="select select-bordered w-full max-w-xs"
on:change={(value) => dispatch("change", value.currentTarget.value)}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>

View File

@ -0,0 +1,97 @@
<script lang="ts">
import { onMount } from "svelte"
let history: string[] = []
let currentDir = "~"
type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear"
const commands: Record<Command, () => string> = {
help: () => `Available commands:
about - Display information about me
skills - List my technical skills
projects - Show my notable projects
contact - Display my contact information
clear - Clear the terminal screen`,
about: () => `Hi, I'm John Doe!
I'm a passionate software developer with 5 years of experience.
I love creating elegant solutions to complex problems.`,
skills: () => `My technical skills include:
- JavaScript/TypeScript
- React & Next.js
- Node.js
- Python
- SQL & NoSQL databases`,
projects: () => `Some of my notable projects:
1. E-commerce Platform (React, Node.js, MongoDB)
2. Weather App (React Native, OpenWeatherMap API)
3. Task Management System (Python, Django, PostgreSQL)`,
contact: () => `You can reach me at:
Email: john.doe@example.com
GitHub: github.com/johndoe
LinkedIn: linkedin.com/in/johndoe`,
clear: () => {
history = []
return ""
},
}
const executeCommand = (input: string) => {
const [command, ...args] = input.trim().split(" ")
if (command in commands) {
return commands[command as Command]()
}
return `Command not found: ${command}. Type 'help' for available commands.`
}
let input = ""
let inputRef: HTMLInputElement | null = null
const handleSubmit = (e: Event) => {
e.preventDefault()
if (input.trim()) {
if (input === "clear") {
history = []
} else {
history = [
...history,
`${currentDir} $ ${input}`,
executeCommand(input),
]
}
input = ""
}
}
onMount(() => {
history = [
"Welcome to John Doe's Terminal Portfolio!",
"Type 'help' to see available commands.",
]
})
$: {
if (inputRef) {
inputRef.scrollIntoView({ behavior: "smooth" })
}
}
</script>
<div class="min-h-screen bg-black text-green-500 p-4 font-mono">
<div class="max-w-3xl mx-auto">
<div class="mb-4">
{#each history as line}
<pre class="whitespace-pre-wrap">{line}</pre>
{/each}
</div>
<form on:submit={handleSubmit} class="flex">
<span class="mr-2">{currentDir} $</span>
<input
bind:this={inputRef}
bind:value={input}
type="text"
class="flex-grow bg-transparent outline-none"
/>
</form>
</div>
</div>

View File

@ -0,0 +1,8 @@
---
interface Props {
tag: string
}
const { tag } = Astro.props
---
<div class="badge badge-outline">{tag}</div>

View File

@ -0,0 +1,12 @@
---
import Badge from "./Badge.astro"
interface Props {
tags: string[]
}
const { tags } = Astro.props
---
<div class="flex flex-wrap gap-1">
{tags.map((tag) => <Badge tag={tag} />)}
</div>

View File

@ -0,0 +1,12 @@
---
interface Props {
href: string
class?: string
}
const { href, class: clazz } = Astro.props
---
<a href={href} target="_blank" rel="noopener" class:list={["link", clazz]}>
<slot />
</a>

View File

@ -0,0 +1,16 @@
---
import ExternalLink from "./ExternalLink.astro"
import * as m from "../../paraglide/messages"
import Gitea from "../../icons/Gitea.astro"
interface Props {
href: string
}
const { href } = Astro.props
---
<div>
<ExternalLink href={href} class="flex items-center gap-1">
<Gitea class="w-6 h-6" />
{m.sourceCode()}
</ExternalLink>
</div>