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

main
Guus van Meerveld 8 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 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 = { module.exports = {
reactStrictMode: true, reactStrictMode: true,
images: {
domains: [process.env.NEXT_PUBLIC_GITEA_SERVER]
}
}; };

@ -1,4 +1,6 @@
{ {
"name": "portfolio-website",
"version": "0.1.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -18,7 +20,7 @@
"@tanstack/react-query": "^4.24.9", "@tanstack/react-query": "^4.24.9",
"axios": "^1.1.2", "axios": "^1.1.2",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"configcat-node": "^8.0.0", "fs-extra": "^11.2.0",
"iron-session": "^6.3.1", "iron-session": "^6.3.1",
"next": "^12.1.0", "next": "^12.1.0",
"next-seo": "^4.24.0", "next-seo": "^4.24.0",
@ -32,13 +34,14 @@
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0", "@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/fs-extra": "^11.0.4",
"@types/node": "^15.12.1", "@types/node": "^15.12.1",
"@types/react": "^17.0.9", "@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6", "@types/react-dom": "^17.0.6",
"eslint": "^7.28.0", "eslint": "^7.28.0",
"eslint-config-next": "13.1.6", "eslint-config-next": "13.1.6",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"prisma": "^4.10.1", "prisma": "^5.8.1",
"sass": "^1.34.1", "sass": "^1.34.1",
"typescript": "^4.3.2" "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 { FC } from "react";
import { giteaServerUrl, giteaUsername } from "@utils/config";
import multipleClassNames from "@utils/multipleClassNames"; import multipleClassNames from "@utils/multipleClassNames";
const Footer: FC = () => { const Footer: FC = () => {
@ -17,9 +16,6 @@ const Footer: FC = () => {
<h3>Guus van Meerveld</h3> <h3>Guus van Meerveld</h3>
</div> </div>
<div className="col col-12"> <div className="col col-12">
<Link href={`https://${giteaServerUrl}/${giteaUsername}`}>
<a className="mr-2">Git</a>
</Link>
&middot; &middot;
<Link href="https://twitter.com/Guusvanmeerveld"> <Link href="https://twitter.com/Guusvanmeerveld">
<a className="mx-2">Twitter</a> <a className="mx-2">Twitter</a>

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

@ -1,17 +1,15 @@
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">
@ -26,43 +24,50 @@ const User: FC<{ isAvailable: boolean; user: z.infer<typeof UserResponse> }> =
> >
<div className={styles.avatarContainer}> <div className={styles.avatarContainer}>
<Image <Image
src={user.avatar_url} src={owner.avatar ?? ""}
className={styles.avatar} className={styles.avatar}
width={256} width={256}
height={256} height={256}
alt={`${user.full_name}'s avatar`} alt={`${owner.name}'s avatar`}
/> />
</div> </div>
</div> </div>
<div className="column col-8 col-md-12 col-mx-auto"> <div className="column col-8 col-md-12 col-mx-auto">
<h1>{user.full_name}</h1> <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"
>
Contact
</Link>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default User; export default User;

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

@ -1,5 +1,12 @@
import z from "zod"; 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({ const Link = z.object({
remoteAddress: z.string().url(), remoteAddress: z.string().url(),
location: z.string() 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"; 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()
}); });

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

@ -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 { GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import { NextSeo } from "next-seo"; 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 { readLandingJson } from "@utils/landing";
import {
fetchAvailability,
fetchRepositories,
fetchUser
} from "@utils/git/fetch";
import BestRepository from "@components/BestRepository";
import FeaturedRepositories from "@components/FeaturedRepositories";
import Layout from "@components/Layout"; import Layout from "@components/Layout";
import User from "@components/User"; import User from "@components/User";
export const getStaticProps: GetStaticProps = async () => { export const getStaticProps: GetStaticProps<Landing> = async () => {
const isAvailable = await fetchAvailability(); const landing = await readLandingJson();
const user = await fetchUser(giteaUsername); if (landing === null) return { revalidate: 1, notFound: true };
const repositories = await fetchRepositories(user.id); return { props: landing };
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
};
}; };
const Index: NextPage = ({ const Index: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (
repositories, landing
user, ) => (
bestRepository,
isAvailable
}: InferGetStaticPropsType<typeof getStaticProps>) => (
<Layout> <Layout>
<NextSeo title="Home" /> <NextSeo title="Home" />
<User isAvailable={isAvailable} user={user} /> <User owner={landing.owner} />
<FeaturedRepositories repositories={repositories} />
<BestRepository repository={bestRepository} />
</Layout> </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 sessionCookieName = "portfolio-session";
export const sessionPassword = process.env.SESSION_PASSWORD ?? ""; export const sessionPassword = process.env.SESSION_PASSWORD ?? "";
@ -20,3 +14,6 @@ export const registrationIsEnabled =
export const saltRoundsForPassword = export const saltRoundsForPassword =
parseInt(process.env.PASSWORD_SALT_ROUNDS ?? "") || 10; 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"; } from "@models/git/responses";
import { giteaServerUrl } from "@utils/config"; import { giteaServerUrl } from "@utils/config";
import createConfigCatClient from "@utils/createConfigCatClient";
const apiUrl = `https://${giteaServerUrl}/api/v1`; 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 ( export const fetchUser = async (
giteaUsername: string giteaUsername: string
): Promise<z.infer<typeof UserResponse>> => { ): 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