feat: cv verifier

This commit is contained in:
Alexander Daichendt 2025-01-01 14:00:05 +01:00
parent 4dd699f08c
commit 194b4b0808
24 changed files with 2199 additions and 70 deletions

View file

@ -0,0 +1,51 @@
---
import type { InferSelectModel } from "drizzle-orm";
import type { cvTable } from "../../db/schema";
interface Props {
cv: InferSelectModel<typeof cvTable>;
}
const { cv } = Astro.props;
---
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6">
<ul class="space-y-4 mb-0">
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32"> UUID: </span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.uuid}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32">
Issued by:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.author}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32">
Issued to:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.company_name}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32">
Issue date:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.created?.toLocaleString()}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32"> Purpose: </span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.purpose}
</span>
</li>
</ul>
</div>

View file

@ -0,0 +1,5 @@
<div class="text-center p-8 bg-red-50 dark:bg-red-950 rounded-lg shadow-sm">
<p class="text-red-600 dark:text-red-400 text-lg font-medium">
No CV found with this UUID.
</p>
</div>

View file

@ -0,0 +1,20 @@
<div class="flex items-center justify-center gap-2 mb-4">
<div class="bg-red-100 dark:bg-red-900 p-2 rounded-full">
<svg
class="w-6 h-6 text-red-600 dark:text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400">Revoked CV</h2>
</div>
<p class="text-red-600 dark:text-red-400 text-lg">
This CV has been revoked and is no longer valid.
</p>

View file

@ -0,0 +1,36 @@
---
import type { InferSelectModel } from "drizzle-orm";
import type { cvTable } from "../../db/schema";
interface Props {
cv: InferSelectModel<typeof cvTable>;
}
const { cv } = Astro.props;
---
<div class="flex items-center gap-2 mb-6">
<div class="bg-green-100 dark:bg-green-900 p-2 rounded-full">
<svg
class="w-6 h-6 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100">
Verified CV
</h2>
</div>
<p class="text-gray-600 dark:text-gray-300 mb-6">
This CV was issued and verified by
<span class="font-semibold">{cv.author}</span>, for{" "}
<span class="font-semibold">{cv.company_name}</span>
</p>

15
src/db/schema.ts Normal file
View file

@ -0,0 +1,15 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const cvTable = sqliteTable("cv", {
uuid: text("uuid").primaryKey(),
company_name: text("company_name").notNull(),
created: integer("created", {
mode: "timestamp_ms",
}),
author: text("author").notNull(),
purpose: text("purpose").notNull(),
tooling: text("tooling").notNull(),
status: text("status", {
enum: ["active", "revoked"],
}).default("active"),
});

10
src/env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
// src/env.d.ts
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
declare namespace App {
interface Locals extends Runtime {}
}

View file

@ -0,0 +1,39 @@
---
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import { cvTable } from "../../db/schema";
import BaseLayout from "../../layouts/BaseLayout.astro";
import DataTable from "../../components/verification/DataTable.astro";
import NoCV from "../../components/verification/NoCV.astro";
import Verified from "../../components/verification/Verified.astro";
import Revoked from "../../components/verification/Revoked.astro";
export const prerender = false;
const uuid = Astro.url.searchParams.get("uuid");
const db = drizzle(Astro.locals.runtime.env.DB);
const cv = uuid
? await db.select().from(cvTable).where(eq(cvTable.uuid, uuid))
: [];
---
<BaseLayout title="CV Verification">
<section class="max-w-4xl mx-auto min-h-screen">
{
cv.length === 0 ? (
<NoCV />
) : cv[0].status !== "active" ? (
<div class="text-center p-8 bg-red-50 dark:bg-red-950 rounded-lg shadow-sm">
<Revoked />
<DataTable cv={cv[0]} />
</div>
) : (
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-lg">
<Verified cv={cv[0]} />
<DataTable cv={cv[0]} />
</div>
)
}
</section>
</BaseLayout>

View file

@ -4,7 +4,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout>
<h1>Hi, my name is Alex!</h1>
<h1 class="text-4xl sm:text-5xl">Hi, my name is Alex!</h1>
<p>
I am a software engineer, Linux enthusiast and a friend of lightweight,
resilient systems.

View file

@ -5,15 +5,15 @@
*/
body {
font-family: "Ubuntu";
margin: 0;
padding: 0;
text-align: left;
background-size: 100% 600px;
word-wrap: break-word;
overflow-wrap: break-word;
font-size: 20px;
line-height: 1.7;
font-family: "Ubuntu";
margin: 0;
padding: 0;
text-align: left;
background-size: 100% 600px;
word-wrap: break-word;
overflow-wrap: break-word;
font-size: 20px;
line-height: 1.7;
}
h1,
@ -22,83 +22,83 @@ h3,
h4,
h5,
h6 {
margin: 0.5rem 0 0.5rem 0;
line-height: 1.2;
margin: 0.5rem 0 0.5rem 0;
line-height: 1.2;
}
h1 {
font-size: 3.052em;
font-size: 3.052em;
}
h2 {
font-size: 2.441em;
font-size: 2.441em;
}
h3 {
font-size: 1.953em;
font-size: 1.953em;
}
h4 {
font-size: 1.563em;
font-size: 1.563em;
}
h5 {
font-size: 1.25em;
font-size: 1.25em;
}
strong,
b {
font-weight: 700;
font-weight: 700;
}
p {
margin-bottom: 0.75em;
margin-bottom: 0.75em;
}
.prose p {
margin-bottom: 2em;
margin-bottom: 2em;
}
textarea {
width: 100%;
font-size: 16px;
width: 100%;
font-size: 16px;
}
input {
font-size: 16px;
font-size: 16px;
}
table {
width: 100%;
width: 100%;
}
img {
max-width: 100%;
height: auto;
border-radius: 8px;
max-width: 100%;
height: auto;
border-radius: 8px;
}
code {
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
}
pre {
padding: 0.5em;
border-radius: 8px;
margin-bottom: 2em;
padding: 0.5em;
border-radius: 8px;
margin-bottom: 2em;
}
pre > code {
all: unset;
all: unset;
}
blockquote {
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
margin: 0px;
font-size: 1.333em;
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
margin: 0px;
font-size: 1.333em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
border: none;
border-top: 1px solid rgb(var(--gray-light));
}
ul:not(ul ul, ol ul),
ol:not(ul ol, ol ol) {
margin-bottom: 2em;
margin-bottom: 2em;
}
.footnotes {
margin-left: 2rem;
margin-left: 2rem;
}
.footnotes ol {
list-style: outside;
list-style: outside;
}
.footnotes li {
margin-top: -3rem;
margin-top: -3rem;
}