Initial commit
This commit is contained in:
@ -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>→</span>
|
||||
</h2>
|
||||
<p>
|
||||
{body}
|
||||
</p>
|
||||
</a>
|
||||
<a href={href}>
|
||||
<h2>
|
||||
{title}
|
||||
<span>→</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>
|
||||
|
37
src/components/ContactMeForm.astro
Normal file
37
src/components/ContactMeForm.astro
Normal 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>
|
9
src/components/Footer.astro
Normal file
9
src/components/Footer.astro
Normal 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>
|
20
src/components/Greeting.astro
Normal file
20
src/components/Greeting.astro
Normal 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>
|
11
src/components/HardwarePage.svelte
Normal file
11
src/components/HardwarePage.svelte
Normal 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} />
|
28
src/components/Input.astro
Normal file
28
src/components/Input.astro
Normal 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>
|
13
src/components/Navbar.astro
Normal file
13
src/components/Navbar.astro
Normal 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>
|
32
src/components/ProjectCard.astro
Normal file
32
src/components/ProjectCard.astro
Normal 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>
|
55
src/components/ProjectPage.astro
Normal file
55
src/components/ProjectPage.astro
Normal 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>
|
13
src/components/Select.svelte
Normal file
13
src/components/Select.svelte
Normal 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>
|
97
src/components/Terminal.svelte
Normal file
97
src/components/Terminal.svelte
Normal 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>
|
8
src/components/badge/Badge.astro
Normal file
8
src/components/badge/Badge.astro
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
interface Props {
|
||||
tag: string
|
||||
}
|
||||
const { tag } = Astro.props
|
||||
---
|
||||
|
||||
<div class="badge badge-outline">{tag}</div>
|
12
src/components/badge/BadgeList.astro
Normal file
12
src/components/badge/BadgeList.astro
Normal 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>
|
12
src/components/links/ExternalLink.astro
Normal file
12
src/components/links/ExternalLink.astro
Normal 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>
|
16
src/components/links/GiteaLink.astro
Normal file
16
src/components/links/GiteaLink.astro
Normal 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>
|
Reference in New Issue
Block a user