Skip to content

Commit

Permalink
feat: added contact form
Browse files Browse the repository at this point in the history
- added slack messages
- updated contentful usage
- moved pages to general group folder
  • Loading branch information
schaechinger committed Dec 7, 2023
1 parent 59fc460 commit fdfe542
Show file tree
Hide file tree
Showing 33 changed files with 856 additions and 259 deletions.
31 changes: 0 additions & 31 deletions app/(de)/kontakt/page.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import LinkButton from '@/app/components/LinkButton';
import { Metadata } from 'next';
import Link from 'next/link';

Expand Down Expand Up @@ -28,18 +29,14 @@ const PrivacyPage = () => (
<section id="verantwortlicher" className="pt-10">
<h3>Verantwortlicher</h3>

<p className="mb-2">
<p className="mb-4">
Manuel Schächinger<br />
An der Ottosäule 16<br />
85521 Ottobrunn
</p>

<p className="mb-2">
<a href="mailto:manuel@schaechinger.com">E-Mail senden</a>
</p>

<p>
<Link href="/impressum">Impressum</Link>
<LinkButton href="/impressum" label="Impressum" />
</p>
</section>

Expand Down
File renamed without changes.
43 changes: 43 additions & 0 deletions app/(pages)/(de)/kontakt/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Metadata } from 'next';

import LinkButton from '@components/LinkButton';
import ContactForm from '@components/forms/ContactForm';

export const metadata: Metadata = {
title: 'Kontakt',
description: 'Hier können Sie auf schnellem Weg mit mir in Kontakt treten.',
};

const ResumePage = () => (
<div className="resume-page pt-4 lg:pt-10 lg:mr-80">
<section id="kontakt">
<h2>Kontakt</h2>

<p className="mb-2">Der erste Schritt liegt bei Ihnen.</p>

<p>
Melden Sie sich gerne und wir setzen Ihr Vorhaben so um, wie Sie es sich vorstellen!
</p>

<div className="flex flex-wrap mt-10 gap-y-10">
<div className="flex-1">
<h3>Per Mail</h3>
<LinkButton href="mailto:manuel@schaechinger.com">E-Mail senden</LinkButton>
</div>

<div className="flex-1">
<h3>Telefonisch</h3>
<LinkButton href="tel:+4916097506593">Jetzt anrufen</LinkButton>
</div>

<div className="flex-none w-full">
<h3>Direkt schreiben</h3>

<ContactForm />
</div>
</div>
</section>
</div>
);

export default ResumePage;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import TechItem from '@components/career/TechItem';

export const metadata: Metadata = {
title: 'Berufserfahrung',
description: 'Erfahren Sie mehr über meinen beruflichen Werdegang.',
};

const ResumePage = () => (
Expand Down
10 changes: 4 additions & 6 deletions app/(de)/page.tsx → app/(pages)/(de)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,17 @@ const Home = () => (
</p>

<p className="mb-2">
In zahlreichen Projekten konnte ich erfolgreich den gesamten Projektzyklus von der Idee
bis hin zum fertigen Produkt begleiten und war sowohl beratend als auch als Entwickler
tätig. Meine Aufgabenbereiche erstrecken sich über komplexe APIs mit <strong
Meine Aufgabenbereiche erstrecken sich über komplexe APIs mit <strong
className="highlight-label">Node.js</strong>, performante Frontends mit <strong
className="highlight-label">Vue</strong> und <strong
className="highlight-label">React</strong>, Absicherung von Systemen sowie Deployment
und Übergabe an Kunden.
</p>

<p className="mb-4">
Abseits der Arbeit bin ich leidenschaftlicher Läufer und trainiere fast immer für den
nächsten Marathon. Mein großes Ziel ist die Teilnahmen an den sechs großen Marathons der
Welt, von denen ich bis auf Boston schon fünf abschließen konnte.
Abseits der Arbeit bin ich leidenschaftlicher Läufer. Mein großes Ziel ist die
Teilnahmen an den sechs großen Marathons der Welt, von denen ich bis auf Boston
schon fünf abschließen konnte.
</p>

<p>
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { notFound } from 'next/navigation';
import LinkButton from '@components/LinkButton';
import DetailBlock from '@components/projects/DetailBlock';
import ProjectMasterData from '@components/projects/ProjectMasterData';
import { getDatabase } from '@lib/db/factory';
import { loadProjectBySlug } from '@/app/lib/contentful';

interface ProjectPageProps {
export interface ProjectPageProps {
params: {
slug: string;
}
Expand All @@ -20,7 +20,7 @@ export async function generateMetadata(
title: 'Projektdetails',
};

const project = await getDatabase()?.loadProjectBySlug(params.slug);
const project = await loadProjectBySlug(params.slug);

if (project) {
metadata.title = project.title;
Expand All @@ -31,7 +31,7 @@ export async function generateMetadata(
};

const ProjectPage = async ({ params }: ProjectPageProps) => {
const project = await getDatabase()?.loadProjectBySlug(params.slug);
const project = await loadProjectBySlug(params.slug);

if (!project) {
return notFound();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,29 @@ import { notFound } from 'next/navigation';

import LinkButton from '@components/LinkButton';
import ProjectMasterData from '@components/projects/ProjectMasterData';
import { getDatabase } from '@lib/db/factory';
import { loadProjectBySlug } from '@/app/lib/contentful';
import { ProjectPageProps } from '../[slug]/page';

export const metadata: Metadata = {
title: 'Internetmarke',
export async function generateMetadata(
{ params }: ProjectPageProps,
): Promise<Metadata> {
const metadata: Metadata = {
title: 'Projektdetails',
};

const project = await loadProjectBySlug('internetmarke');

if (project) {
metadata.title = project.title;
metadata.description = project.description;
}

return metadata;
};

const InternetmarkePage = async () => {
unstable_noStore();
const project = await getDatabase()?.loadProjectBySlug('internetmarke');
const project = await loadProjectBySlug('internetmarke');

if (!project) {
return notFound();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ProjectList from '@components/projects/ProjectList';

export const metadata: Metadata = {
title: 'Projekte',
description: 'Erhalten Sie hier einen Überblick über meine bisherigen Projekte.',
}

const ProjectsPage = () => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,29 @@ import { notFound } from 'next/navigation';

import LinkButton from '@components/LinkButton';
import ProjectMasterData from '@components/projects/ProjectMasterData';
import { getDatabase } from '@lib/db/factory';
import { loadProjectBySlug } from '@/app/lib/contentful';
import { ProjectPageProps } from '../[slug]/page';

export const metadata: Metadata = {
title: 'TransportKit',
export async function generateMetadata(
{ params }: ProjectPageProps,
): Promise<Metadata> {
const metadata: Metadata = {
title: 'Projektdetails',
};

const project = await loadProjectBySlug('transportkit');

if (project) {
metadata.title = project.title;
metadata.description = project.description;
}

return metadata;
};

const TransportKitPage = async () => {
unstable_noStore();
const project = await getDatabase()?.loadProjectBySlug('transportkit');
const project = await loadProjectBySlug('transportkit');

if (!project) {
return notFound();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ import { Metadata } from 'next';
import LinkButton from '@components/LinkButton';

export const metadata: Metadata = {
title: 'Gehen wir es an!',
title: 'Meine Tätigkeit',
description: 'Ein Einblick in meine Tätigkeitsfelder sowie meine technischen Fähigkeiten.',
};

const ResumePage = () => (
<div className="resume-page pt-4 lg:pt-10 lg:mr-80">
<section id="taetigkeit">
<h2>Tätigkeit</h2>
<h2>Meine Tätigkeit</h2>

<p className="mb-2">
In zahlreichen Projekten konnte ich erfolgreich den gesamten Projektzyklus von der Idee
bis hin zum fertigen Produkt begleiten und war sowohl beratend als auch als Entwickler
tätig.
</p>

<p>
Im Rahmen meiner freiberuflichen Tätigkeit kann ich entweder auf Stundenbasis oder auf
Expand Down Expand Up @@ -62,6 +69,51 @@ const ResumePage = () => (
<LinkButton href="/projekte">Meine bisherigen Projekte</LinkButton>
</p>
</section>

<section id="mein-stack" className="mt-10">
<h2>Meine Basisstack</h2>

<div>
<h3>Frontend</h3>
<div className="border-b border-b-primary-200 pb-1 mb-1">
Vue, Quasar, Pinia, Sass, ...
</div>
<div>
React, Next.js, Sass, ...
</div>
</div>

<div className="mt-6">
<h3>Backend</h3>
<div className="border-b border-b-primary-200 pb-1 mb-1">
Node.js, Express, MQTT, ...
</div>
<div>
Strapi, Contentful, ...
</div>
</div>

<div className="mt-6">
<h3>Datenbanken</h3>
<div>
PostgreSQL, DynamoDB, MongoDB, Neo4j, ...
</div>
</div>

<div className="mt-6">
<h3>CI & CD</h3>
<div>
Docker, GitHub Actions, Mocha, ...
</div>
</div>

<div className="mt-6">
<h3>Basis</h3>
<div>
Linux, AWS, Traefik, Git, REST, Elasticsearch, Auth0, Jira, Confluence, ...
</div>
</div>
</section>
</div>
);

Expand Down
40 changes: 40 additions & 0 deletions app/actions/submitContact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use server';

import { ZodError, z } from 'zod';
import { sendMessage } from '../lib/slack';

export interface ContactFormState {
success: boolean;
message?: string;
field?: string;
}

const ContactMessage = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(5),
});

export const submitContact = async (state: Awaited<ContactFormState>, payload: FormData) => {
try {
const parsed = ContactMessage.parse({
name: payload.get('name'),
email: payload.get('email'),
message: payload.get('message'),
});

const message = `Neue Kontakt-Anfrage: *${parsed.name}* (${parsed.email}):\n${parsed.message}`;

await sendMessage(message);

return { success: true };
} catch (e) {
if (e instanceof ZodError) {
const [error] = e.errors;

return { success: false, field: error.path[0] } as ContactFormState;
} else {
return { success: false, field: 'form' };
}
}
};
14 changes: 10 additions & 4 deletions app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import Link from 'next/link';
import SocialLinks from '@components/SocialLinks';
import HeartEmpty from '@components/icons/HeartEmpty';
import SnesButtons from '@components/mario/SnesButtons';
import ThemeToggle from './ThemeToggle';

const Footer = () => (
<footer className="footer pb-6 mt-10 sm:text-left text-sm">
<footer className="footer mb-6 mt-10 sm:text-left text-sm">

<SnesButtons />

<div className="lg:hidden">
<div className="mt-6 lg:hidden">
<SocialLinks />
</div>

<ul className="flex justify-start gap-4 mt-10 mb-2">
<div className="mt-6 lg:hidden">
<ThemeToggle />
</div>

<ul className="flex justify-start gap-4 mt-6 mb-2">
<li>
<Link href="/impressum" className="font-normal">
Impressum
Expand All @@ -29,7 +34,8 @@ const Footer = () => (
&copy; { new Date().getFullYear() } Manuel Schächinger. Alle Rechte vorbehalten.
</p>
<p className="footer__love">Mit viel <HeartEmpty
className="text-lg -mt-1 mx-1 text-snes-a transition-colors" /> in München entwickelt.</p>
className="text-lg -mt-1 mx-1 text-snes-a"
/> in München entwickelt.</p>
</footer>
);

Expand Down
Loading

0 comments on commit fdfe542

Please sign in to comment.