refactor(v4.0.0-alpha): beginning of a new era

This commit is contained in:
Amruth Pillai
2023-11-05 12:31:42 +01:00
parent 0ba6a444e2
commit 22933bd412
505 changed files with 81829 additions and 0 deletions

4
libs/templates/.babelrc Normal file
View File

@ -0,0 +1,4 @@
{
"presets": [["@nx/react/babel", { "runtime": "automatic", "useBuiltIns": "usage" }]],
"plugins": [["styled-components", { "pure": true, "ssr": false }]]
}

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,16 @@
{
"name": "@reactive-resume/templates",
"version": "0.0.1",
"private": false,
"main": "./index.js",
"types": "./index.d.ts",
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}

View File

@ -0,0 +1,40 @@
{
"name": "templates",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/templates/src",
"projectType": "library",
"tags": ["frontend"],
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/templates/**/*.{ts,tsx,js,jsx}"]
}
},
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/libs/templates"
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/libs/templates"
}
}
}
}

View File

@ -0,0 +1,3 @@
export * from "./shared";
export * from "./styles";
export * from "./templates";

View File

@ -0,0 +1,49 @@
import { ResumeData } from "@reactive-resume/schema";
import { useEffect, useMemo } from "react";
import { FrameContextConsumer } from "react-frame-component";
import { StyleSheetManager } from "styled-components";
import { GlobalStyles } from "../styles";
import { GlobalStyleProps } from "../styles/shared";
import { FrameWrapper } from "./frame";
import { useStore } from "./store";
type Props = {
resume: ResumeData;
children: React.ReactNode;
style?: React.CSSProperties;
};
export const Artboard = ({ resume, style, children }: Props) => {
const store = useStore();
const metadata = useStore((state) => state.metadata);
const styles: GlobalStyleProps | null = useMemo(() => {
if (!metadata) return null;
return {
$css: metadata.css,
$page: metadata.page,
$theme: metadata.theme,
$typography: metadata.typography,
};
}, [metadata]);
useEffect(() => useStore.setState(resume), [resume]);
if (Object.keys(store).length === 0 || !styles) return;
return (
<FrameWrapper style={style}>
<FrameContextConsumer>
{({ document }) => (
<StyleSheetManager target={document?.head}>
<GlobalStyles {...styles} />
{children}
</StyleSheetManager>
)}
</FrameContextConsumer>
</FrameWrapper>
);
};

View File

@ -0,0 +1,83 @@
import { pageSizeMap } from "@reactive-resume/utils";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Frame from "react-frame-component";
import webfontloader from "webfontloader";
import { useStore } from "./store";
type Props = {
children: React.ReactNode;
style?: React.CSSProperties;
};
export const FrameWrapper = ({ children, style }: Props) => {
const resume = useStore();
const format = resume.metadata.page.format;
const font = resume.metadata.typography.font;
const fontString = useMemo(() => {
const family = font.family;
const variants = font.variants.join(",");
const subset = font.subset;
return `${family}:${variants}:${subset}`;
}, [font]);
const frameRef = useRef<HTMLIFrameElement | null>(null);
const width = useMemo(() => `${pageSizeMap[format].width}mm`, [format]);
const [height, setHeight] = useState(`${pageSizeMap[format].height}mm`);
const handleResize = useCallback((frame: HTMLIFrameElement) => {
const height = frame.contentDocument?.body?.scrollHeight ?? 0;
setHeight(`${height}px`);
}, []);
const onLoad = useCallback(() => {
if (!frameRef.current) return;
handleResize(frameRef.current);
if (font.family === "CMU Serif") {
return webfontloader.load({
classes: false,
custom: {
families: ["CMU Serif"],
urls: ["https://cdn.jsdelivr.net/npm/computer-modern/cmu-serif.min.css"],
},
context: frameRef.current.contentWindow!,
fontactive: () => {
handleResize(frameRef.current!);
},
});
}
webfontloader.load({
classes: false,
google: { families: [fontString] },
context: frameRef.current.contentWindow!,
fontactive: () => {
handleResize(frameRef.current!);
},
});
}, [frameRef, font, fontString, handleResize]);
useEffect(() => {
onLoad();
}, [resume, onLoad]);
useEffect(() => {
setTimeout(onLoad, 250);
}, [onLoad]);
return (
<Frame
ref={frameRef}
onLoad={onLoad}
onLoadedData={onLoad}
style={{ width, height, pointerEvents: "none", ...style }}
>
{children}
</Frame>
);
};

View File

@ -0,0 +1,4 @@
export * from "./artboard";
export * from "./frame";
export * from "./store";
export * from "./templates";

View File

@ -0,0 +1,4 @@
import { ResumeData } from "@reactive-resume/schema";
import { create } from "zustand";
export const useStore = create<ResumeData>()(() => ({}) as ResumeData);

View File

@ -0,0 +1,6 @@
import { SectionKey } from "@reactive-resume/schema";
export type TemplateProps = {
isFirstPage?: boolean;
columns: SectionKey[][];
};

View File

@ -0,0 +1,14 @@
import styled from "styled-components";
export const ItemGrid = styled.div<{ $columns?: number }>`
display: grid;
grid-gap: 16px;
grid-template-columns: repeat(${({ $columns }) => $columns ?? 1}, 1fr);
`;
export const PageGrid = styled.div<{ $offset: number }>`
display: flex;
flex-direction: row;
align-items: flex-start;
column-gap: ${({ $offset }) => $offset}px;
`;

View File

@ -0,0 +1,13 @@
import { createGlobalStyle } from "styled-components";
import { Reset } from "./reset";
import { GlobalStyleProps, Shared } from "./shared";
export const GlobalStyles = createGlobalStyle<GlobalStyleProps>`
${Reset}
${Shared}
`;
export * from "./grid";
export * from "./page";
export * from "./picture";

View File

@ -0,0 +1,59 @@
import styled from "styled-components";
export const PageWrapper = styled.div`
position: relative;
width: var(--page-width);
padding: var(--page-margin);
min-height: var(--page-height);
/* Theme */
color: var(--color-text);
background-color: var(--color-background);
@media print {
margin: 0 auto;
&:not(:last-child) {
height: var(--page-height);
}
}
`;
export const PageNumber = styled.p`
top: 0;
right: 0;
color: black;
font-size: 12px;
font-weight: 600;
padding: 0 0.5rem;
position: absolute;
outline: 1px solid black;
background-color: white;
`;
export const PageBreakLine = styled.div<{ $pageHeight: number }>`
position: absolute;
top: ${({ $pageHeight }) => $pageHeight}mm;
left: 0;
right: 0;
z-index: 10;
border: 1px dashed var(--color-text);
/* Text */
&:before {
content: "End of Page";
background: white;
color: black;
display: block;
font-size: 12px;
font-weight: 600;
height: auto;
line-height: 0rem;
padding: 12px 16px;
position: absolute;
right: 0;
text-align: right;
top: -25px;
}
`;

View File

@ -0,0 +1,11 @@
import { Basics } from "@reactive-resume/schema";
import styled from "styled-components";
export const Picture = styled.img<{ $picture: Basics["picture"] }>`
width: ${({ $picture }) => $picture.size}px;
aspect-ratio: ${({ $picture }) => $picture.aspectRatio};
border-radius: ${({ $picture }) => $picture.borderRadius}px;
${({ $picture }) => $picture.effects.grayscale && `filter: grayscale(1);`}
${({ $picture }) => $picture.effects.border && `border: 2px solid var(--color-primary);`}
`;

View File

@ -0,0 +1,122 @@
import { css } from "styled-components";
export const Reset = css`
/***
The new CSS reset - version 1.11.1 (last updated 24.10.2023)
GitHub page: https://github.com/elad2412/the-new-css-reset
***/
/*
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
- The "symbol *" part is to solve Firefox SVG sprite bug
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
*/
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
all: unset;
display: revert;
}
/* Preferred box-sizing value */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Fix mobile Safari increase font-size on landscape mode */
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
font-size: var(--font-size);
font-family: var(--font-family);
}
body {
overflow: hidden;
}
/* Reapply the pointer cursor for anchor tags */
a,
button {
cursor: revert;
}
/* Remove list styles (bullets/numbers) */
ol,
ul,
menu,
summary {
list-style: none;
}
/* For images to not be able to exceed their container */
img {
max-inline-size: 100%;
max-block-size: 100%;
}
/* removes spacing between cells in tables */
table {
border-collapse: collapse;
}
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
input,
textarea {
user-select: auto;
-webkit-user-select: auto;
}
/* revert the 'white-space' property for textarea elements on Safari */
textarea {
white-space: revert;
}
/* minimum style to allow to style meter element */
meter {
-webkit-appearance: revert;
appearance: revert;
}
/* preformatted text - use only for this feature */
:where(pre) {
all: revert;
box-sizing: border-box;
}
/* reset default text opacity of input placeholder */
::placeholder {
color: unset;
}
/* fix the feature of 'hidden' attribute.
display:revert; revert to element instead of attribute */
:where([hidden]) {
display: none;
}
/* revert for bug in Chromium browsers
- fix for the content editable attribute will work properly.
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
:where([contenteditable]:not([contenteditable="false"])) {
-moz-user-modify: read-write;
-webkit-user-modify: read-write;
overflow-wrap: break-word;
line-break: after-white-space;
-webkit-line-break: after-white-space;
user-select: auto;
-webkit-user-select: auto;
}
/* apply back the draggable feature - exist only in Chromium and Safari */
:where([draggable="true"]) {
-webkit-user-drag: element;
}
/* Revert Modal native behavior */
:where(dialog:modal) {
all: revert;
box-sizing: border-box;
}
`;

View File

@ -0,0 +1,150 @@
import { Metadata } from "@reactive-resume/schema";
import { pageSizeMap } from "@reactive-resume/utils";
import { css } from "styled-components";
export type GlobalStyleProps = {
$css: Metadata["css"];
$page: Metadata["page"];
$theme: Metadata["theme"];
$typography: Metadata["typography"];
};
export const Shared = css<GlobalStyleProps>`
/* CSS Variables */
:root {
/* Theme */
--color-text: ${({ $theme }) => $theme.text};
--color-primary: ${({ $theme }) => $theme.primary};
--color-background: ${({ $theme }) => $theme.background};
/* Page */
--page-width: ${({ $page }) => pageSizeMap[$page.format].width}mm;
--page-height: ${({ $page }) => pageSizeMap[$page.format].height}mm;
--page-margin: ${({ $page }) => $page.margin}px;
/* Typography */
--font-size: ${({ $typography }) => $typography.font.size}px;
--font-family: ${({ $typography }) => $typography.font.family};
--line-height: ${({ $typography }) => $typography.lineHeight}rem;
}
/* Headings */
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: bold;
line-height: var(--line-height);
}
h1 {
font-size: 2rem;
line-height: calc(var(--line-height) + 1.5rem);
}
h2 {
font-size: 1.5rem;
line-height: calc(var(--line-height) + 1rem);
}
h3 {
font-size: 1rem;
line-height: calc(var(--line-height) + 0rem);
}
/* Paragraphs */
p {
font-size: var(--font-size);
line-height: var(--line-height);
}
b,
strong {
font-weight: bold;
}
small {
font-size: calc(var(--font-size) - 2px);
line-height: calc(var(--line-height) - 0.5rem);
}
i,
em {
font-style: italic;
}
u {
text-decoration: underline;
text-underline-offset: 1.5px;
}
a {
text-decoration: ${({ $typography }) => ($typography.underlineLinks ? "underline" : "none")};
text-underline-offset: 1.5px;
}
s,
del {
text-decoration: line-through;
}
pre,
code {
font-family: monospace;
}
pre code {
display: block;
padding: 1rem;
border-radius: 4px;
color: white;
background-color: black;
}
mark {
color: black;
background-color: #fcd34d;
}
/* Lists */
menu,
ol,
ul {
list-style: disc inside;
li {
margin: 0.25rem 0;
line-height: var(--line-height);
p {
display: inline;
line-height: var(--line-height);
}
}
menu,
ol,
ul {
list-style: circle inside;
li {
padding-left: 32px;
}
}
}
/* Horizontal Rules */
hr {
margin: 0.5rem 0;
border: 0.5px solid currentColor;
}
/* Images */
img {
display: block;
max-width: 100%;
object-fit: cover;
}
`;

View File

@ -0,0 +1 @@
export * from "./rhyhorn";

View File

@ -0,0 +1,56 @@
import { SectionKey } from "@reactive-resume/schema";
import { TemplateProps } from "../../shared";
import { Awards } from "./sections/awards";
import { Certifications } from "./sections/certifications";
import { CustomSection } from "./sections/custom";
import { Education } from "./sections/education";
import { Experience } from "./sections/experience";
import { Header } from "./sections/header";
import { Interests } from "./sections/interests";
import { Languages } from "./sections/languages";
import { Profiles } from "./sections/profiles";
import { Projects } from "./sections/projects";
import { Publications } from "./sections/publications";
import { References } from "./sections/references";
import { Skills } from "./sections/skills";
import { Summary } from "./sections/summary";
import { Volunteer } from "./sections/volunteer";
import { RhyhornStyles } from "./style";
const sectionMap: Partial<Record<SectionKey, () => React.ReactNode>> = {
summary: Summary,
profiles: Profiles,
experience: Experience,
education: Education,
awards: Awards,
skills: Skills,
certifications: Certifications,
interests: Interests,
languages: Languages,
volunteer: Volunteer,
projects: Projects,
publications: Publications,
references: References,
};
const getSection = (id: SectionKey) => {
const Section = sectionMap[id];
// Custom Section
if (!Section) return <CustomSection key={id} id={id} />;
return <Section key={id} />;
};
export const Rhyhorn = ({ isFirstPage, columns }: TemplateProps) => (
<RhyhornStyles>
{isFirstPage && <Header />}
{/* Main */}
{columns[0].map(getSection)}
{/* Sidebar */}
{columns[1].map(getSection)}
</RhyhornStyles>
);

View File

@ -0,0 +1,35 @@
import { Award as IAward } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Awards = () => {
const section = useStore((state) => state.sections.awards);
return (
<SectionBase<IAward>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.title}</h6>
</a>
) : (
<h6>{item.title}</h6>
)}
<p>{item.awarder}</p>
</div>
<div>
<h6>{item.date}</h6>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,35 @@
import { Certification as ICertification } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Certifications = () => {
const section = useStore((state) => state.sections.certifications);
return (
<SectionBase<ICertification>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.name}</h6>
</a>
) : (
<h6>{item.name}</h6>
)}
<p>{item.issuer}</p>
</div>
<div>
<h6>{item.date}</h6>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,53 @@
import {
CustomSection as ICustomSection,
CustomSectionItem,
SectionKey,
} from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
type Props = {
id: SectionKey;
};
export const CustomSection = ({ id }: Props) => {
const section = useStore((state) => get(state.sections, id));
if (!section) return null;
return (
// @ts-expect-error Unable to infer type of Custom Section accurately, ignoring for now
<SectionBase<ICustomSection>
section={section}
header={(item: CustomSectionItem) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.name}</h6>
</a>
) : (
<h6>{item.name}</h6>
)}
<p>{item.description}</p>
</div>
<div>
<h6>{item.date}</h6>
<div className="rating">
{Array.from({ length: item.level }).map((_, i) => (
<span key={i} />
))}
</div>
</div>
</Fragment>
)}
main={(item: CustomSectionItem) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
footer={(item: CustomSectionItem) => <small>{item.keywords.join(", ")}</small>}
/>
);
};

View File

@ -0,0 +1,39 @@
import { Education as IEducation } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Education = () => {
const section = useStore((state) => state.sections.education);
return (
<SectionBase<IEducation>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.institution}</h6>
</a>
) : (
<h6>{item.institution}</h6>
)}
<p>{item.area}</p>
</div>
<div>
<h6>{item.date}</h6>
<p>
{item.studyType}
{item.score ? ` | ${item.score}` : ""}
</p>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,36 @@
import { Experience as IExperience } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Experience = () => {
const section = useStore((state) => state.sections.experience);
return (
<SectionBase<IExperience>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.company}</h6>
</a>
) : (
<h6>{item.company}</h6>
)}
<p>{item.position}</p>
</div>
<div>
<h6>{item.date}</h6>
<p>{item.location}</p>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,56 @@
import { Picture, useStore } from "@reactive-resume/templates";
export const Header = () => {
const basics = useStore((state) => state.basics);
return (
<div className="header">
{basics.picture.url && !basics.picture.effects.hidden && (
<Picture
alt={basics.name}
src={basics.picture.url}
$picture={basics.picture}
className="header__picture"
/>
)}
<div className="header__basics">
<h1 className="header__name">{basics.name}</h1>
<p className="header__headline">{basics.headline}</p>
<div className="header__meta">
{basics.location && <span className="header__meta-location">{basics.location}</span>}
{basics.phone && (
<span className="header__meta-phone">
<a href={`tel:${basics.phone}`} target="_blank" rel="noopener noreferrer nofollow">
{basics.phone}
</a>
</span>
)}
{basics.email && (
<span className="header__meta-email">
<a href={`mailto:${basics.email}`} target="_blank" rel="noopener noreferrer nofollow">
{basics.email}
</a>
</span>
)}
{basics.url.href && (
<span className="header__meta-url">
<a href={basics.url.href} target="_blank" rel="noopener noreferrer nofollow">
{basics.url.label || basics.url.href}
</a>
</span>
)}
</div>
<div className="header__meta custom-fields">
{basics.customFields.map((field) => (
<span key={field.id} className="header__meta custom-field">
{[field.name, field.value].filter(Boolean).join(": ")}
</span>
))}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,25 @@
import { Interest as IInterest } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Interests = () => {
const section = useStore((state) => state.sections.interests);
return (
<SectionBase<IInterest>
section={section}
header={(item) => (
<Fragment>
<div>
<h6>{item.name}</h6>
<p>{item.keywords.join(", ")}</p>
</div>
<div />
</Fragment>
)}
/>
);
};

View File

@ -0,0 +1,26 @@
import { Language as ILanguage } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { getCEFRLevel } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Languages = () => {
const section = useStore((state) => state.sections.languages);
return (
<SectionBase<ILanguage>
section={section}
header={(item) => (
<Fragment>
<div>
<h6>{item.name}</h6>
<p>{item.fluency || getCEFRLevel(item.fluencyLevel)}</p>
</div>
<div />
</Fragment>
)}
/>
);
};

View File

@ -0,0 +1,49 @@
import { Profile as IProfile } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import styled from "styled-components";
import { SectionBase } from "../shared/section-base";
const Username = styled.h6`
line-height: 1;
font-weight: 500;
`;
export const Profiles = () => {
const section = useStore((state) => state.sections.profiles);
return (
<SectionBase<IProfile>
section={section}
header={(item) => (
<Fragment>
<div>
{item.icon && (
<i>
<img
width="16"
height="16"
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
</i>
)}
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<Username>{item.username}</Username>
</a>
) : (
<Username>{item.username}</Username>
)}
<small>{item.network}</small>
</div>
<div />
</Fragment>
)}
/>
);
};

View File

@ -0,0 +1,36 @@
import { Project as IProject } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Projects = () => {
const section = useStore((state) => state.sections.projects);
return (
<SectionBase<IProject>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.name}</h6>
</a>
) : (
<h6>{item.name}</h6>
)}
<p>{item.description}</p>
</div>
<div>
<h6>{item.date}</h6>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
footer={(item) => <small>{item.keywords.join(", ")}</small>}
/>
);
};

View File

@ -0,0 +1,35 @@
import { Publication as IPublication } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Publications = () => {
const section = useStore((state) => state.sections.publications);
return (
<SectionBase<IPublication>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.name}</h6>
</a>
) : (
<h6>{item.name}</h6>
)}
<p>{item.publisher}</p>
</div>
<div>
<h6>{item.date}</h6>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,33 @@
import { Reference as IReference } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const References = () => {
const section = useStore((state) => state.sections.references);
return (
<SectionBase<IReference>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.name}</h6>
</a>
) : (
<h6>{item.name}</h6>
)}
<p>{item.description}</p>
</div>
<div />
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,26 @@
import { Skill as ISkill } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Skills = () => {
const section = useStore((state) => state.sections.skills);
return (
<SectionBase<ISkill>
section={section}
header={(item) => (
<Fragment>
<div>
<h6>{item.name}</h6>
<p>{item.description}</p>
</div>
<div />
</Fragment>
)}
footer={(item) => <small>{item.keywords.join(", ")}</small>}
/>
);
};

View File

@ -0,0 +1,22 @@
import { useStore } from "@reactive-resume/templates";
import { Heading } from "../shared/heading";
export const Summary = () => {
const section = useStore((state) => state.sections.summary);
if (!section.visible || !section.content) return null;
return (
<section id={section.id} className={`section section__${section.id}`}>
<Heading>{section.name}</Heading>
<main className="section__item-content">
<div
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</main>
</section>
);
};

View File

@ -0,0 +1,36 @@
import { Volunteer as IVolunteer } from "@reactive-resume/schema";
import { useStore } from "@reactive-resume/templates";
import { isUrl } from "@reactive-resume/utils";
import { Fragment } from "react";
import { SectionBase } from "../shared/section-base";
export const Volunteer = () => {
const section = useStore((state) => state.sections.volunteer);
return (
<SectionBase<IVolunteer>
section={section}
header={(item) => (
<Fragment>
<div>
{isUrl(item.url.href) ? (
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
<h6>{item.organization}</h6>
</a>
) : (
<h6>{item.organization}</h6>
)}
<p>{item.position}</p>
</div>
<div>
<h6>{item.date}</h6>
<p>{item.location}</p>
</div>
</Fragment>
)}
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
/>
);
};

View File

@ -0,0 +1,10 @@
import styled from "styled-components";
export const Heading = styled.h4`
font-size: 0.9rem;
line-height: 1.2rem;
padding-bottom: 2px;
margin-bottom: 6px;
text-transform: uppercase;
border-bottom: 1px solid var(--color-text);
`;

View File

@ -0,0 +1,33 @@
import { Item, SectionItem, SectionWithItem } from "@reactive-resume/schema";
import { ItemGrid } from "@reactive-resume/templates";
import { Heading } from "./heading";
type Props<T extends Item> = {
section: SectionWithItem<T>;
header?: (item: T) => React.ReactNode;
main?: (item: T) => React.ReactNode;
footer?: (item: T) => React.ReactNode;
};
export const SectionBase = <T extends SectionItem>({ section, header, main, footer }: Props<T>) => {
if (!section.visible || !section.items.length) return null;
return (
<section id={section.id} className={`section section__${section.id}`}>
<Heading>{section.name}</Heading>
<ItemGrid className="section__items" $columns={section.columns}>
{section.items
.filter((item) => !!item.visible)
.map((item) => (
<div key={item.id} className="section__item">
{header && <header className="section__item-header">{header(item as T)}</header>}
{main && <main className="section__item-main">{main(item as T)}</main>}
{footer && <footer className="section__item-footer">{footer(item as T)}</footer>}
</div>
))}
</ItemGrid>
</section>
);
};

View File

@ -0,0 +1,91 @@
import styled from "styled-components";
export const RhyhornStyles = styled.div`
display: grid;
row-gap: 16px;
.header {
display: flex;
&__picture {
align-self: center;
margin-right: 12px;
}
&__basics {
align-self: center;
}
&__name {
font-size: 1.5rem;
line-height: calc(var(--line-height) + 0.5rem);
}
&__meta {
font-size: 0.875rem;
line-height: var(--line-height);
span {
padding: 0 6px;
border-right: 2px solid var(--color-primary);
&:first-child {
padding-left: 0;
}
&:last-child {
border-right: none;
}
}
}
}
.section {
&__item {
display: flex;
flex-direction: column;
gap: 4px;
&-header {
position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
& > div:last-child {
text-align: right;
}
&:has(i) div {
margin-left: 22px;
}
& > div > i {
position: absolute;
top: 6px;
left: 0;
}
}
&-content p:not(:last-child),
&-main p:not(:last-child) {
padding-bottom: 0.75rem;
}
& .rating {
display: flex;
justify-content: flex-end;
margin-top: 4px;
& > span {
width: 8px;
height: 8px;
margin-left: 4px;
border-radius: 50%;
border: 1px solid currentColor;
}
}
}
}
`;

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client", "vitest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}

View File

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}

View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
},
"include": [
"vite.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,39 @@
/// <reference types='vitest' />
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react";
import * as path from "path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
cacheDir: "../../node_modules/.vite/templates",
plugins: [
react(),
nxViteTsPaths(),
dts({
entryRoot: "src",
tsconfigPath: path.join(__dirname, "tsconfig.lib.json"),
}),
],
build: {
lib: {
entry: "src/index.ts",
name: "templates",
fileName: "index",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["react", "react-dom", "react/jsx-runtime"],
},
},
test: {
globals: true,
environment: "jsdom",
cache: { dir: "../../node_modules/.vitest" },
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
},
});