Skip to content

Commit

Permalink
sync latest changes
Browse files Browse the repository at this point in the history
  • Loading branch information
brignano authored Jul 21, 2024
1 parent 0a8a1d4 commit 3b80089
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 84 deletions.
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
LOG_LEVEL=info
REACT_EDITOR=code
API_URL="http://localhost:3000/api"
# update below values in .env.local
JSON_RESUME_URL=""
GOOGLE_MAPS_KEY=""
GOOGLE_API_KEY=""
13 changes: 7 additions & 6 deletions app/_components/_about/about.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import LoadingSpinner from "@/app/_components/loading-spinner";
import { PhotoIcon } from "@heroicons/react/20/solid";
import SocialIcons from "@/app/_components/_about/social-icons";
import Socials from "@/app/_components/_about/socials";
import { Metadata } from "next";
import { AboutSchema } from "@/app/_utils/resume-schema";
import { AboutSchema } from "@/app/_utils/schemas";

interface AboutProps {
about?: AboutSchema;
Expand Down Expand Up @@ -35,18 +35,19 @@ export default function About(props: AboutProps) {
{/*
todo: update to use Image, dynamically set remotePatterns in next.config.mjs
*/}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
style={imageStyle}
src={about.image}
src={about.image_url}
alt="Selfie"
width={50}
height={50}
width={90}
/>

<div className="container">
<h1 className="pb-1 text-3xl md:text-5xl font-semibold font-mono leading-7 text-gray-900">
{about.name}
</h1>
<SocialIcons email={about.email} socials={about.socials} />
<Socials social_urls={about.social_urls} />
</div>
</div>

Expand Down
26 changes: 26 additions & 0 deletions app/_components/_about/location.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LocationSchema } from "@/app/_utils/schemas";

interface LocationProps {
location: LocationSchema;
}

export default function Location(props: LocationProps) {
const { location } = props;

return (
<div role="none" className="h-screen flex justify-center items-center">
<div>
<iframe
width="600"
height="450"
style={{ border: 0 }}
src={`https://www.google.com/maps/embed/v1/place?q=${location.city},${location.state},${location.country}&key=${servercon}`}
/>
</div>
<div>
{location.city}, {location.state}{" "}
{location.country && `(${location.country})`}
</div>
</div>
);
}
36 changes: 0 additions & 36 deletions app/_components/_about/social-icons.tsx

This file was deleted.

35 changes: 35 additions & 0 deletions app/_components/_about/socials.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SocialIcon } from "react-social-icons";

interface SocialsProps {
social_urls?: string[];
}

export default function Socials(props: SocialsProps) {
const { social_urls } = props;

return (
<div className="container inline-flex">
{social_urls?.map((url, index) => {
return (
<>
<SocialIcon
url={
url.includes("@") && !url.startsWith("mailto:")
? `mailto:${url}`
: url
}
key={index}
aria-hidden="true"
style={{
width: "1.25rem",
height: "1.25rem",
marginLeft: "0.25rem",
}}
className="hover:opacity-50"
/>
</>
);
})}
</div>
);
}
7 changes: 6 additions & 1 deletion app/_utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export const logger = require("pino")();
import pino from "pino";

export const logger = pino({
level: process.env.LOG_LEVEL || "info",
base: null,
});
48 changes: 23 additions & 25 deletions app/_utils/resume-schema.ts → app/_utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,18 @@ export type LocationSchema = z.infer<typeof LocationSchema>;
const LocationSchema = z.object({
city: z.string(),
state: z.string(),
country: z.optional(z.string()),
});

export type AboutSchema = z.infer<typeof AboutSchema>;
const AboutSchema = z.object({
name: z.string(),
title: z.optional(z.string()),
image: z.string(),
email: z.string(),
image_url: z.string().url(),
social_urls: z.optional(z.array(z.string())),
website: z.optional(z.string()),
summary: z.optional(z.string()),
location: z.optional(LocationSchema.merge(z.object({ country: z.string() }))),
socials: z.array(
z.object({
site: z.string(),
username: z.string(),
})
),
location: z.optional(LocationSchema),
});

export type JobSchema = z.infer<typeof JobSchema>;
Expand Down Expand Up @@ -52,14 +47,8 @@ const SkillSchema = InterestSchema.merge(
})
);

export type SocialSchema = z.infer<typeof SocialSchema>;
const SocialSchema = z.object({
site: z.string(),
username: z.string(),
});

export type ResumeSchema = z.infer<typeof ResumeSchema>;
const ResumeSchema = z.object({
export const ResumeSchema = z.object({
about: AboutSchema,
jobs: z.optional(z.array(JobSchema)),
skills: z.optional(z.array(SkillSchema)),
Expand All @@ -71,22 +60,31 @@ const ResumeSchema = z.object({
*
* @param resume
* @returns a boolean indicating whether the resume is valid
* @throws an error if the resume is invalid as per the schema
*/
export const isValidResume = (resume: unknown) => {
logger.info(resume);
export const validateResume = (resume: unknown) => {
logger.debug(`Validating schema...`);
const result = ResumeSchema.safeParse(resume);

if (result.error) {
logger.warn(result.error.format());
logger.error(
`Invalid JSON Schema: ${JSON.stringify(
result.error.flatten().fieldErrors,
null,
2
)}`
);
throw new Error("Invalid JSON Schema");
}

return result.success;
};

export async function parseResume(resume: unknown) {
if (!isValidResume(resume)) {
throw new Error("Invalid JSON schema.");
}

return ResumeSchema.parse(resume);
export async function parseResume(resumeJson: unknown) {
logger.debug(`API Response: ${JSON.stringify(resumeJson, null, 2)}`);
logger.info(`Loading resume...`);
validateResume(resumeJson);
const resume = ResumeSchema.parse(resumeJson);
logger.debug(`Using Resume: ${JSON.stringify(resume, null, 2)}`);
return resume;
}
27 changes: 19 additions & 8 deletions app/api/resume/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { logger } from "@/app/_utils/logger";
import { Resume } from "@/resume";
import { ReasonPhrases, StatusCodes } from "http-status-codes";
import getConfig from "next/config";
import { parseResume } from "@/app/_utils/schemas";

export async function GET() {
const { publicRuntimeConfig } = getConfig();

if (!publicRuntimeConfig.jsonResumeUrl) {
logger.error(`Failed to find environment variable: JSON_RESUME_URL`);
return new Response(
`Failed to find environment variable: JSON_RESUME_URL`,
{
status: StatusCodes.INTERNAL_SERVER_ERROR,
statusText: ReasonPhrases.INTERNAL_SERVER_ERROR,
}
);
}

const response = await fetch(publicRuntimeConfig.jsonResumeUrl);
logger.info("Fetching resume...");
logger.debug(publicRuntimeConfig.jsonResumeUrl);

const response = await fetch(publicRuntimeConfig.jsonResumeUrl, {
//! todo: remove cache strategy
cache: "no-cache",
});

if (!response.ok) {
return new Response(
`Failed to find Resume. Please validate the URL exists and is publically accessible: ${publicRuntimeConfig.jsonResumeUrl}`,
`Failed to GET JSON file. Please validate the URL exists (and is publically accessible): ${publicRuntimeConfig.jsonResumeUrl}`,
{
status: StatusCodes.NOT_FOUND,
statusText: ReasonPhrases.NOT_FOUND,
Expand All @@ -23,12 +35,11 @@ export async function GET() {
}

try {
const jsonResume = await response.json();
logger.debug(`jsonResume: ${JSON.stringify(jsonResume, null, 2)}`);
return Response.json(jsonResume as Resume);
const resume = await parseResume(await response.json());
return Response.json(resume);
} catch (e) {
return new Response(
`Failed to parse JSON. Please validate the file is in a validate JSON format: ${publicRuntimeConfig.jsonResumeUrl}`,
`${(e as Error).message}: ${publicRuntimeConfig.jsonResumeUrl}`,
{
status: StatusCodes.NOT_FOUND,
statusText: ReasonPhrases.NOT_FOUND,
Expand Down
43 changes: 36 additions & 7 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
import About from "@/app/_components/_about/about";
import { parseResume } from "@/app/_utils/resume-schema";
import { Suspense } from "react";
import LoadingSpinner from "./_components/loading-spinner";
import { parseResume } from "./_utils/schemas";
import getConfig from "next/config";

const getResume = async () => {
const response = await fetch(process.env.API_URL + `/resume`, {
next: {
cache: "no-cache",
},
//! todo: remove cache strategy
cache: "no-cache",
});
const resumeJson = await response.json();
return parseResume(resumeJson);

if (!response.ok) {
return null;
}

// return await response.json();
return parseResume(await response.json());
};

export default async function Resume() {
const { publicRuntimeConfig } = getConfig();
const resume = await getResume();

if (!resume) {
return (
<main>
<div className="container mx-auto flex justify-center items-center h-screen">
<div role="alert" className="w-3/4">
<div className="bg-red-500 text-white font-bold rounded-t px-4 py-2">
Failed to load resume!
</div>
<div className="border border-t-0 border-red-400 rounded-b bg-red-100 px-4 py-3 text-red-700 overflow-auto">
<a href={publicRuntimeConfig.jsonResumeUrl} target="_blank">
{publicRuntimeConfig.jsonResumeUrl}
</a>
</div>
</div>
</div>
</main>
);
}

return (
<main>
<div className="container">
<About about={resume.about} />
<Suspense fallback={<LoadingSpinner />}>
<About about={resume.about} />
</Suspense>
</div>
</main>
);
Expand Down
3 changes: 3 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
const nextConfig = {
publicRuntimeConfig: {
jsonResumeUrl: process.env.JSON_RESUME_URL
},
serverRuntimeConfig: {
googleApiKey: process.env.GOOGLE_API_KEY
}
};

Expand Down
Loading

0 comments on commit 3b80089

Please sign in to comment.