started on revamping landing page
continuous-integration/drone/push Build encountered an error
Details
continuous-integration/drone/push Build encountered an error
Details
parent
3dc83ace74
commit
7c6a4ae9cf
@ -1,3 +1,2 @@
|
|||||||
NEXT_PUBLIC_GITEA_USERNAME=Guusvanmeerveld
|
DATABASE_URL=postgresql://portfolio:portfolio@localhost:5432/portfolio?schema=public
|
||||||
NEXT_PUBLIC_GITEA_SERVER=git.guusvanmeerveld.dev
|
LANDING_JSON_LOCATION=./landing.json
|
||||||
DATABASE_URL=postgresql://portfolio:portfolio@localhost:5432/portfolio?schema=public
|
|
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"owner": {
|
||||||
|
"fullName": "Guus van Meerveld",
|
||||||
|
"name": "guus",
|
||||||
|
"description": "AI student at Radboud University. Creating software as a hobby.",
|
||||||
|
"contact": {
|
||||||
|
"email": "contact@guusvanmeerveld.dev",
|
||||||
|
"git": "https://github.com/Guusvanmeerveld",
|
||||||
|
"linkedin": "https://linkedin.com/in/guus-van-meerveld-038357210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { format as formatTimeAgo } from "timeago.js";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
import { RepositoryResponse } from "@models/git/responses";
|
|
||||||
|
|
||||||
const BestRepository: FC<{ repository: z.infer<typeof RepositoryResponse> }> =
|
|
||||||
({ repository }) => {
|
|
||||||
return (
|
|
||||||
<div className="hero bg-primary">
|
|
||||||
<div className="container">
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column col-8 col-md-12 col-mx-auto">
|
|
||||||
<h3 className="text-secondary">My most popular project:</h3>
|
|
||||||
<h1>{repository.full_name}</h1>
|
|
||||||
|
|
||||||
<h5>{repository.description}</h5>
|
|
||||||
<p className="text-secondary">
|
|
||||||
Written in {repository.language}, has{" "}
|
|
||||||
{repository.open_issues_count} issue(s) and{" "}
|
|
||||||
{repository.forks_count} fork(s).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Link href={repository.html_url}>
|
|
||||||
<a className="btn mr-2">Github</a>
|
|
||||||
</Link>
|
|
||||||
{repository.website && (
|
|
||||||
<Link href={repository.website}>
|
|
||||||
<a className="btn">Website</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h5 className="text-secondary">
|
|
||||||
Last updated {formatTimeAgo(repository.updated_at, "en_US")}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BestRepository;
|
|
@ -1,71 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { format as formatTimeAgo } from "timeago.js";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import styles from "./repositories.module.scss";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
import { RepositoryResponse } from "@models/git/responses";
|
|
||||||
|
|
||||||
import multipleClassNames from "@utils/multipleClassNames";
|
|
||||||
|
|
||||||
const FeaturedRepositories: FC<{
|
|
||||||
repositories: z.infer<typeof RepositoryResponse>[];
|
|
||||||
}> = ({ repositories }) => {
|
|
||||||
return (
|
|
||||||
<div className={multipleClassNames("container", styles.main)}>
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column col-6 col-mx-auto text-center">
|
|
||||||
<h3>Some of my featured projects:</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column col-9 col-mx-auto">
|
|
||||||
<div className="columns">
|
|
||||||
{repositories.map((repository) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={repository.full_name}
|
|
||||||
className="column col-3 col-md-12 col-mx-auto mb-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={multipleClassNames(
|
|
||||||
"card",
|
|
||||||
"text-center",
|
|
||||||
styles.card
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="card-header text-primary">
|
|
||||||
<div className="card-title h5">
|
|
||||||
{repository.full_name}
|
|
||||||
</div>
|
|
||||||
<div className="card-subtitle text-gray">
|
|
||||||
{(repository.size / 1024).toPrecision(2)} MB - Last
|
|
||||||
updated {formatTimeAgo(repository.updated_at, "en_US")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">{repository.description}</div>
|
|
||||||
<div className="card-footer">
|
|
||||||
<Link href={repository.html_url}>
|
|
||||||
<a className="btn btn-primary">Git</a>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{repository.website && (
|
|
||||||
<Link href={repository.website}>
|
|
||||||
<a className="btn btn-primary ml-2">Website</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeaturedRepositories;
|
|
@ -1,68 +1,73 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import z from "zod";
|
import Link from "next/link";
|
||||||
|
|
||||||
import styles from "./user.module.scss";
|
import styles from "./user.module.scss";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
import { UserResponse } from "@models/git/responses";
|
import Owner from "@models/owner";
|
||||||
|
|
||||||
import { giteaServerUrl } from "@utils/config";
|
|
||||||
import multipleClassNames from "@utils/multipleClassNames";
|
import multipleClassNames from "@utils/multipleClassNames";
|
||||||
|
|
||||||
const User: FC<{ isAvailable: boolean; user: z.infer<typeof UserResponse> }> =
|
const User: FC<{ owner: Owner }> = ({ owner }) => {
|
||||||
({ isAvailable, user }) => {
|
return (
|
||||||
return (
|
<div className={styles.main}>
|
||||||
<div className={styles.main}>
|
<div className="container">
|
||||||
<div className="container">
|
<div className="columns">
|
||||||
<div className="columns">
|
<div
|
||||||
<div
|
className={multipleClassNames(
|
||||||
className={multipleClassNames(
|
"column",
|
||||||
"column",
|
"col-4",
|
||||||
"col-4",
|
"col-mx-auto",
|
||||||
"col-mx-auto",
|
styles.avatarCol
|
||||||
styles.avatarCol
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<div className={styles.avatarContainer}>
|
||||||
<div className={styles.avatarContainer}>
|
<Image
|
||||||
<Image
|
src={owner.avatar ?? ""}
|
||||||
src={user.avatar_url}
|
className={styles.avatar}
|
||||||
className={styles.avatar}
|
width={256}
|
||||||
width={256}
|
height={256}
|
||||||
height={256}
|
alt={`${owner.name}'s avatar`}
|
||||||
alt={`${user.full_name}'s avatar`}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="column col-8 col-md-12 col-mx-auto">
|
</div>
|
||||||
<h1>{user.full_name}</h1>
|
<div className="column col-8 col-md-12 col-mx-auto">
|
||||||
|
<h1>{owner.fullName}</h1>
|
||||||
|
|
||||||
<h3>{user.description}</h3>
|
<h3>{owner.description}</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
href={`https://${giteaServerUrl}/${user.login}`}
|
href={owner.contact.git}
|
||||||
className="btn btn-primary mr-2"
|
className="btn btn-primary mr-2"
|
||||||
>
|
>
|
||||||
Git
|
Git
|
||||||
</a>
|
</Link>
|
||||||
|
|
||||||
<a href={`mailto:${user.email}`} className="btn btn-primary">
|
<Link
|
||||||
Contact
|
target="_blank"
|
||||||
</a>
|
rel="noreferrer"
|
||||||
</p>
|
href={owner.contact.linkedin}
|
||||||
|
className="btn btn-primary mr-2"
|
||||||
|
>
|
||||||
|
Git
|
||||||
|
</Link>
|
||||||
|
|
||||||
<p className="text-gray">
|
<Link
|
||||||
Availibility: {isAvailable && "Available"}
|
href={`mailto:${owner.contact.email}`}
|
||||||
{!isAvailable && "Not available"}
|
className="btn btn-primary"
|
||||||
</p>
|
>
|
||||||
</div>
|
Contact
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
.body {
|
.body {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
@ -1,29 +0,0 @@
|
|||||||
import z from "zod";
|
|
||||||
|
|
||||||
export const RepositoryResponse = z.object({
|
|
||||||
full_name: z.string(),
|
|
||||||
html_url: z.string(),
|
|
||||||
language: z.string(),
|
|
||||||
website: z.string(),
|
|
||||||
created_at: z.string(),
|
|
||||||
updated_at: z.string(),
|
|
||||||
forks_count: z.number(),
|
|
||||||
open_issues_count: z.number(),
|
|
||||||
stars_count: z.number(),
|
|
||||||
description: z.string(),
|
|
||||||
size: z.number()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SearchResultsResponse = z.object({
|
|
||||||
ok: z.boolean(),
|
|
||||||
data: RepositoryResponse.array()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const UserResponse = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
login: z.string(),
|
|
||||||
email: z.string(),
|
|
||||||
avatar_url: z.string(),
|
|
||||||
full_name: z.string(),
|
|
||||||
description: z.string()
|
|
||||||
});
|
|
@ -0,0 +1,11 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { OwnerModel } from "./owner";
|
||||||
|
|
||||||
|
export const LandingModel = z.object({
|
||||||
|
owner: OwnerModel
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Landing = z.infer<typeof LandingModel>;
|
||||||
|
|
||||||
|
export default Landing;
|
@ -0,0 +1,17 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export const OwnerModel = z.object({
|
||||||
|
fullName: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
avatar: z.string().optional(),
|
||||||
|
contact: z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
linkedin: z.string().url(),
|
||||||
|
git: z.string().url()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Owner = z.infer<typeof OwnerModel>;
|
||||||
|
|
||||||
|
export default Owner;
|
@ -1,8 +1,8 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export const Post = z.object({
|
export const Post = z.object({
|
||||||
title: z.string(),
|
title: z.string().trim(),
|
||||||
content: z.string().min(100),
|
content: z.string().min(100).trim(),
|
||||||
tags: z.string(),
|
tags: z.string().trim(),
|
||||||
publish: z.boolean()
|
publish: z.boolean()
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import { NextApiHandler } from "next";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { LinkIdFromString } from "@models/link";
|
||||||
|
import { Response } from "@models/response";
|
||||||
|
|
||||||
|
import { methodNotAllowed, unauthorized } from "@utils/errors";
|
||||||
|
import prisma from "@utils/prisma";
|
||||||
|
import { withIronSession } from "@utils/session";
|
||||||
|
|
||||||
|
const handler: NextApiHandler<Response> = async (req, res) => {
|
||||||
|
if (req.method?.toUpperCase() !== "DELETE") {
|
||||||
|
res.status(405).json(methodNotAllowed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = req.session.user;
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
res.status(401).json(unauthorized);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsePostDataResult = z
|
||||||
|
.object({
|
||||||
|
id: LinkIdFromString
|
||||||
|
})
|
||||||
|
.safeParse(req.query);
|
||||||
|
|
||||||
|
if (!parsePostDataResult.success) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ ok: false, error: parsePostDataResult.error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = await prisma.link.findUnique({
|
||||||
|
where: { id: parsePostDataResult.data.id },
|
||||||
|
include: { author: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (link === null) {
|
||||||
|
res.status(400).json({ ok: false, error: "Link does not exist" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.authorId !== user.id) {
|
||||||
|
res.status(401).json(unauthorized);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.link
|
||||||
|
.delete({ where: { id: link.id } })
|
||||||
|
.then(() => res.json({ ok: true, data: "Successfully deleted link" }))
|
||||||
|
.catch((error) => res.status(500).json({ ok: false, error }));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withIronSession(handler);
|
@ -0,0 +1,26 @@
|
|||||||
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { LinkIdFromString } from "@models/link";
|
||||||
|
|
||||||
|
import { withSessionSsr } from "@utils/session";
|
||||||
|
|
||||||
|
const EditLinkPage: NextPage = () => {
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
||||||
|
async ({ req, params }) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
|
||||||
|
if (user === undefined) return { notFound: true };
|
||||||
|
|
||||||
|
const parseParamsResult = z
|
||||||
|
.object({ id: LinkIdFromString })
|
||||||
|
.safeParse(params);
|
||||||
|
|
||||||
|
if (!parseParamsResult.success) return { notFound: true };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EditLinkPage;
|
@ -1,21 +0,0 @@
|
|||||||
import * as configcat from "configcat-node";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
||||||
const createConfigCatClient = () => {
|
|
||||||
if (!process.env.CONFIG_CAT_SDK_KEY) return;
|
|
||||||
|
|
||||||
const logger = configcat.createConsoleLogger(
|
|
||||||
process.env.NODE_ENV == "production" ? 0 : 3
|
|
||||||
);
|
|
||||||
|
|
||||||
const configCatClient = configcat.createClient(
|
|
||||||
process.env.CONFIG_CAT_SDK_KEY,
|
|
||||||
{
|
|
||||||
logger
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return configCatClient;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createConfigCatClient;
|
|
@ -0,0 +1,7 @@
|
|||||||
|
import { stat } from "fs-extra";
|
||||||
|
|
||||||
|
export const exists = async (fileName: string): Promise<boolean> => {
|
||||||
|
return await stat(fileName)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
import { readJson } from "fs-extra";
|
||||||
|
|
||||||
|
import Landing, { LandingModel } from "@models/landing";
|
||||||
|
|
||||||
|
import { landingJsonLocation } from "@utils/config";
|
||||||
|
import { exists } from "@utils/exists";
|
||||||
|
|
||||||
|
export const readLandingJson = async (): Promise<Landing | null> => {
|
||||||
|
const location = landingJsonLocation;
|
||||||
|
|
||||||
|
const fileExists = await exists(location);
|
||||||
|
|
||||||
|
if (!fileExists) {
|
||||||
|
console.log(`Could not find landing json file at: ${location}`);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawJson: unknown = await readJson(location);
|
||||||
|
|
||||||
|
const landingResult = LandingModel.safeParse(rawJson);
|
||||||
|
|
||||||
|
if (!landingResult.success) {
|
||||||
|
console.log(`Failed to parse landing json: ${landingResult.error}`);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return landingResult.data;
|
||||||
|
};
|
Loading…
Reference in new issue