started on revamping landing page
continuous-integration/drone/push Build encountered an error Details

main
Guus van Meerveld 9 months ago
parent 3dc83ace74
commit 7c6a4ae9cf

@ -1,3 +1,2 @@
NEXT_PUBLIC_GITEA_USERNAME=Guusvanmeerveld
NEXT_PUBLIC_GITEA_SERVER=git.guusvanmeerveld.dev
DATABASE_URL=postgresql://portfolio:portfolio@localhost:5432/portfolio?schema=public
LANDING_JSON_LOCATION=./landing.json

@ -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"
}
}
}

@ -4,7 +4,4 @@
**/
module.exports = {
reactStrictMode: true,
images: {
domains: [process.env.NEXT_PUBLIC_GITEA_SERVER]
}
};

@ -1,4 +1,6 @@
{
"name": "portfolio-website",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"dev": "next dev",
@ -18,7 +20,7 @@
"@tanstack/react-query": "^4.24.9",
"axios": "^1.1.2",
"bcrypt": "^5.1.0",
"configcat-node": "^8.0.0",
"fs-extra": "^11.2.0",
"iron-session": "^6.3.1",
"next": "^12.1.0",
"next-seo": "^4.24.0",
@ -32,13 +34,14 @@
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/bcrypt": "^5.0.0",
"@types/fs-extra": "^11.0.4",
"@types/node": "^15.12.1",
"@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6",
"eslint": "^7.28.0",
"eslint-config-next": "13.1.6",
"prettier": "^2.3.1",
"prisma": "^4.10.1",
"prisma": "^5.8.1",
"sass": "^1.34.1",
"typescript": "^4.3.2"
}

@ -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;

@ -4,7 +4,6 @@ import styles from "./footer.module.scss";
import { FC } from "react";
import { giteaServerUrl, giteaUsername } from "@utils/config";
import multipleClassNames from "@utils/multipleClassNames";
const Footer: FC = () => {
@ -17,9 +16,6 @@ const Footer: FC = () => {
<h3>Guus van Meerveld</h3>
</div>
<div className="col col-12">
<Link href={`https://${giteaServerUrl}/${giteaUsername}`}>
<a className="mr-2">Git</a>
</Link>
&middot;
<Link href="https://twitter.com/Guusvanmeerveld">
<a className="mx-2">Twitter</a>

@ -9,6 +9,8 @@ import axios from "axios";
import { Link as LinkType } from "@prisma/client";
import { LinkId } from "@models/link";
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
import useUser from "@utils/hooks/useUser";
import multipleClassNames from "@utils/multipleClassNames";
@ -19,7 +21,7 @@ const LinkComponent: FC<{ link: LinkType }> = ({ link }) => {
const [deleted, setDeleted] = useState(false);
const deleteLink = useCallback(async (id) => {
const parseUserInputResult = z.number().safeParse(id);
const parseUserInputResult = LinkId.safeParse(id);
if (!parseUserInputResult.success) {
setError(parseUserInputResult.error.message);
@ -27,7 +29,7 @@ const LinkComponent: FC<{ link: LinkType }> = ({ link }) => {
}
const response = await axios
.post("/api/link/delete", { id })
.delete("/api/link/delete", { params: { id } })
.then(parseAxiosResponse)
.catch(parseAxiosError);
@ -52,10 +54,12 @@ const LinkComponent: FC<{ link: LinkType }> = ({ link }) => {
</div>
{user !== null && (
<div className="col col-1">
<Link href="/link/edit">
<Link href={`/link/edit/${link.id}`}>
<a className="mr-2">edit</a>
</Link>
<a onClick={() => deleteLink(link.id)}>delete</a>
<a className={styles.delete} onClick={() => deleteLink(link.id)}>
delete
</a>
</div>
)}
</div>

@ -1,68 +1,73 @@
import Image from "next/image";
import z from "zod";
import Link from "next/link";
import styles from "./user.module.scss";
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";
const User: FC<{ isAvailable: boolean; user: z.infer<typeof UserResponse> }> =
({ isAvailable, user }) => {
return (
<div className={styles.main}>
<div className="container">
<div className="columns">
<div
className={multipleClassNames(
"column",
"col-4",
"col-mx-auto",
styles.avatarCol
)}
>
<div className={styles.avatarContainer}>
<Image
src={user.avatar_url}
className={styles.avatar}
width={256}
height={256}
alt={`${user.full_name}'s avatar`}
/>
</div>
const User: FC<{ owner: Owner }> = ({ owner }) => {
return (
<div className={styles.main}>
<div className="container">
<div className="columns">
<div
className={multipleClassNames(
"column",
"col-4",
"col-mx-auto",
styles.avatarCol
)}
>
<div className={styles.avatarContainer}>
<Image
src={owner.avatar ?? ""}
className={styles.avatar}
width={256}
height={256}
alt={`${owner.name}'s avatar`}
/>
</div>
<div className="column col-8 col-md-12 col-mx-auto">
<h1>{user.full_name}</h1>
</div>
<div className="column col-8 col-md-12 col-mx-auto">
<h1>{owner.fullName}</h1>
<h3>{user.description}</h3>
<h3>{owner.description}</h3>
<p>
<a
target="_blank"
rel="noreferrer"
href={`https://${giteaServerUrl}/${user.login}`}
className="btn btn-primary mr-2"
>
Git
</a>
<p>
<Link
target="_blank"
rel="noreferrer"
href={owner.contact.git}
className="btn btn-primary mr-2"
>
Git
</Link>
<a href={`mailto:${user.email}`} className="btn btn-primary">
Contact
</a>
</p>
<Link
target="_blank"
rel="noreferrer"
href={owner.contact.linkedin}
className="btn btn-primary mr-2"
>
Git
</Link>
<p className="text-gray">
Availibility: {isAvailable && "Available"}
{!isAvailable && "Not available"}
</p>
</div>
<Link
href={`mailto:${owner.contact.email}`}
className="btn btn-primary"
>
Contact
</Link>
</p>
</div>
</div>
</div>
);
};
</div>
);
};
export default User;

@ -2,3 +2,7 @@
padding: 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;

@ -1,5 +1,12 @@
import z from "zod";
export const LinkId = z.number().int().positive();
export const LinkIdFromString = z.preprocess(
(a) => parseInt(z.string().parse(a)),
LinkId
);
const Link = z.object({
remoteAddress: z.string().url(),
location: z.string()

@ -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";
export const Post = z.object({
title: z.string(),
content: z.string().min(100),
tags: z.string(),
title: z.string().trim(),
content: z.string().min(100).trim(),
tags: z.string().trim(),
publish: z.boolean()
});

@ -37,8 +37,7 @@ const handle: NextApiHandler<Response> = async (req, res) => {
create: {
...data,
published: publish,
tags: data.tags.trim().split(" "),
content: data.content.trim()
tags: data.tags.split(" ")
}
}
}

@ -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);

@ -1,55 +1,27 @@
import { GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import { NextSeo } from "next-seo";
import z from "zod";
import { RepositoryResponse } from "@models/git/responses";
import Landing from "@models/landing";
import { giteaUsername } from "@utils/config";
import {
fetchAvailability,
fetchRepositories,
fetchUser
} from "@utils/git/fetch";
import { readLandingJson } from "@utils/landing";
import BestRepository from "@components/BestRepository";
import FeaturedRepositories from "@components/FeaturedRepositories";
import Layout from "@components/Layout";
import User from "@components/User";
export const getStaticProps: GetStaticProps = async () => {
const isAvailable = await fetchAvailability();
export const getStaticProps: GetStaticProps<Landing> = async () => {
const landing = await readLandingJson();
const user = await fetchUser(giteaUsername);
if (landing === null) return { revalidate: 1, notFound: true };
const repositories = await fetchRepositories(user.id);
const bestRepository: z.infer<typeof RepositoryResponse> | undefined =
repositories.reduce((prev, current) =>
prev.stars_count > current.stars_count ? prev : current
);
return {
props: {
isAvailable,
user,
bestRepository,
repositories
},
revalidate: 60 * 5
};
return { props: landing };
};
const Index: NextPage = ({
repositories,
user,
bestRepository,
isAvailable
}: InferGetStaticPropsType<typeof getStaticProps>) => (
const Index: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (
landing
) => (
<Layout>
<NextSeo title="Home" />
<User isAvailable={isAvailable} user={user} />
<FeaturedRepositories repositories={repositories} />
<BestRepository repository={bestRepository} />
<User owner={landing.owner} />
</Layout>
);

@ -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,9 +1,3 @@
export const giteaServerUrl =
process.env.NEXT_PUBLIC_GITEA_SERVER ?? "https://git.guusvanmeerveld.dev";
export const giteaUsername =
process.env.NEXT_PUBLIC_GITEA_USERNAME ?? "Guusvanmeerveld";
export const sessionCookieName = "portfolio-session";
export const sessionPassword = process.env.SESSION_PASSWORD ?? "";
@ -20,3 +14,6 @@ export const registrationIsEnabled =
export const saltRoundsForPassword =
parseInt(process.env.PASSWORD_SALT_ROUNDS ?? "") || 10;
export const landingJsonLocation =
process.env.LANDING_JSON_LOCATION ?? "/app/landing.json";

@ -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);
};

@ -9,19 +9,9 @@ import {
} from "@models/git/responses";
import { giteaServerUrl } from "@utils/config";
import createConfigCatClient from "@utils/createConfigCatClient";
const apiUrl = `https://${giteaServerUrl}/api/v1`;
export const fetchAvailability = async (): Promise<boolean> => {
const configCatClient = createConfigCatClient();
const isAvailable: boolean =
(await configCatClient?.getValueAsync("amiavailable", true)) ?? true;
return isAvailable;
};
export const fetchUser = async (
giteaUsername: string
): Promise<z.infer<typeof UserResponse>> => {

@ -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;
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save