mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-16 09:41:31 +10:00
refactor(v4.0.0-alpha): beginning of a new era
This commit is contained in:
4
libs/templates/.babelrc
Normal file
4
libs/templates/.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": [["@nx/react/babel", { "runtime": "automatic", "useBuiltIns": "usage" }]],
|
||||
"plugins": [["styled-components", { "pure": true, "ssr": false }]]
|
||||
}
|
||||
18
libs/templates/.eslintrc.json
Normal file
18
libs/templates/.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
libs/templates/package.json
Normal file
16
libs/templates/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
libs/templates/project.json
Normal file
40
libs/templates/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
libs/templates/src/index.ts
Normal file
3
libs/templates/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./shared";
|
||||
export * from "./styles";
|
||||
export * from "./templates";
|
||||
49
libs/templates/src/shared/artboard.tsx
Normal file
49
libs/templates/src/shared/artboard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
83
libs/templates/src/shared/frame.tsx
Normal file
83
libs/templates/src/shared/frame.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
4
libs/templates/src/shared/index.ts
Normal file
4
libs/templates/src/shared/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./artboard";
|
||||
export * from "./frame";
|
||||
export * from "./store";
|
||||
export * from "./templates";
|
||||
4
libs/templates/src/shared/store.ts
Normal file
4
libs/templates/src/shared/store.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { ResumeData } from "@reactive-resume/schema";
|
||||
import { create } from "zustand";
|
||||
|
||||
export const useStore = create<ResumeData>()(() => ({}) as ResumeData);
|
||||
6
libs/templates/src/shared/templates.ts
Normal file
6
libs/templates/src/shared/templates.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
|
||||
export type TemplateProps = {
|
||||
isFirstPage?: boolean;
|
||||
columns: SectionKey[][];
|
||||
};
|
||||
14
libs/templates/src/styles/grid.ts
Normal file
14
libs/templates/src/styles/grid.ts
Normal 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;
|
||||
`;
|
||||
13
libs/templates/src/styles/index.tsx
Normal file
13
libs/templates/src/styles/index.tsx
Normal 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";
|
||||
59
libs/templates/src/styles/page.ts
Normal file
59
libs/templates/src/styles/page.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
11
libs/templates/src/styles/picture.ts
Normal file
11
libs/templates/src/styles/picture.ts
Normal 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);`}
|
||||
`;
|
||||
122
libs/templates/src/styles/reset.ts
Normal file
122
libs/templates/src/styles/reset.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
150
libs/templates/src/styles/shared.ts
Normal file
150
libs/templates/src/styles/shared.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
1
libs/templates/src/templates/index.ts
Normal file
1
libs/templates/src/templates/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./rhyhorn";
|
||||
56
libs/templates/src/templates/rhyhorn/index.tsx
Normal file
56
libs/templates/src/templates/rhyhorn/index.tsx
Normal 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>
|
||||
);
|
||||
35
libs/templates/src/templates/rhyhorn/sections/awards.tsx
Normal file
35
libs/templates/src/templates/rhyhorn/sections/awards.tsx
Normal 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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
53
libs/templates/src/templates/rhyhorn/sections/custom.tsx
Normal file
53
libs/templates/src/templates/rhyhorn/sections/custom.tsx
Normal 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>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
39
libs/templates/src/templates/rhyhorn/sections/education.tsx
Normal file
39
libs/templates/src/templates/rhyhorn/sections/education.tsx
Normal 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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
36
libs/templates/src/templates/rhyhorn/sections/experience.tsx
Normal file
36
libs/templates/src/templates/rhyhorn/sections/experience.tsx
Normal 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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
56
libs/templates/src/templates/rhyhorn/sections/header.tsx
Normal file
56
libs/templates/src/templates/rhyhorn/sections/header.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
25
libs/templates/src/templates/rhyhorn/sections/interests.tsx
Normal file
25
libs/templates/src/templates/rhyhorn/sections/interests.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
26
libs/templates/src/templates/rhyhorn/sections/languages.tsx
Normal file
26
libs/templates/src/templates/rhyhorn/sections/languages.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
49
libs/templates/src/templates/rhyhorn/sections/profiles.tsx
Normal file
49
libs/templates/src/templates/rhyhorn/sections/profiles.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
36
libs/templates/src/templates/rhyhorn/sections/projects.tsx
Normal file
36
libs/templates/src/templates/rhyhorn/sections/projects.tsx
Normal 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>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
33
libs/templates/src/templates/rhyhorn/sections/references.tsx
Normal file
33
libs/templates/src/templates/rhyhorn/sections/references.tsx
Normal 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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
26
libs/templates/src/templates/rhyhorn/sections/skills.tsx
Normal file
26
libs/templates/src/templates/rhyhorn/sections/skills.tsx
Normal 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>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
22
libs/templates/src/templates/rhyhorn/sections/summary.tsx
Normal file
22
libs/templates/src/templates/rhyhorn/sections/summary.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
36
libs/templates/src/templates/rhyhorn/sections/volunteer.tsx
Normal file
36
libs/templates/src/templates/rhyhorn/sections/volunteer.tsx
Normal 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 }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
10
libs/templates/src/templates/rhyhorn/shared/heading.tsx
Normal file
10
libs/templates/src/templates/rhyhorn/shared/heading.tsx
Normal 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);
|
||||
`;
|
||||
33
libs/templates/src/templates/rhyhorn/shared/section-base.tsx
Normal file
33
libs/templates/src/templates/rhyhorn/shared/section-base.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
91
libs/templates/src/templates/rhyhorn/style.ts
Normal file
91
libs/templates/src/templates/rhyhorn/style.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
21
libs/templates/tsconfig.json
Normal file
21
libs/templates/tsconfig.json
Normal 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"
|
||||
}
|
||||
23
libs/templates/tsconfig.lib.json
Normal file
23
libs/templates/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
19
libs/templates/tsconfig.spec.json
Normal file
19
libs/templates/tsconfig.spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
39
libs/templates/vite.config.ts
Normal file
39
libs/templates/vite.config.ts
Normal 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}"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user