🌊

JSONとReactで履歴書を自作

2021/07/16に公開

要約

JSON 形式で履歴書用のデータを記述して React でページを作成。PDF 形式でそれを保存します。
データは Github で管理するので紛失の心配がありません。

成果物

名前や住所はマスクしますがこのような履歴書ができました。

使用技術

  • React
  • Chakra UI
  • TypeScript

きっかけ

履歴書のデータの管理、日本語対応できれいなデザインの履歴書の欠如、個人情報を渡す不安が自作のきっかけです。

細かく見ていくと、まず、履歴書を書こうとしたところ、過去の履歴書のデータが見つかりませんでした。

そのうえ、きれいな履歴書のテンプレートを探そうとすると、英語のレジュメのみヒットしました。問題は:

  • 項目名が英語固定で日本語に変えられない
  • 項目が固定されているものが多く、帯に短したすきに長しといった印象
  • フォームを埋めるだけできれいな履歴書ができるサービスがあるが、履歴書のために個人情報を与えるのは腰が引けた

一方で日本語で軽く検索すると、義務教育からの経歴を書くといったものも多くヒットした印象でした。
プロジェクトやスキルセットを強調する形式とは正反対で、今回求めるものとは違いました。

そのため、Github でデータをプライベートで容易に管理して、ビューとデータを分離することでデザインを変更できる自由度をもたせ、項目等でも柔軟に調整できる自作レジュメに踏み切りました。

実際のところ、あれこれ悩んで探すよりも自作のほうが結局早く(半日程度)完成しました。

コード

デザインは 2 カラムの A4 で 1 枚に収まる量に調整しています。

他のファイルで定義した型を読み込んで、Chakra UI のコンポーネントを使い、一部カスタムコンポーネントを定義することでデザインを調整したうえで繰り返しの記述量を削減。これでコードの見通しをスッキリさせています。

とはいえ、そもそもコード量も少ないのでほぼ全て App.tsx に集約させています。

データはローカルの json ファイル から読み込んでいます。

コード全文
App.tsx
import {WorkType, EducationType, NaturalLanguageType, ProjectType } from './types/index';
import { Box, Divider, Flex, Tag, Text, Heading, VStack, ListItem, UnorderedList } from "@chakra-ui/react"
import { AtSignIcon, CalendarIcon, EmailIcon, PhoneIcon, LinkIcon } from '@chakra-ui/icons'
import StarRating from 'react-svg-star-rating'
import data from './data/resume.json'
import {H3, H4, TimeSpan } from './components/index';

function App() {
  const {name, label, email, phone, website, address} = data.basics;

  const summary:string = data.summary;
  const works:WorkType[] = data.work;
  const educations:EducationType[] = data.education;
  const programingLanguages:string[] = data.programming_language;
  const naturalLanguages:NaturalLanguageType[] = data.natural_language;
  const techStacks:string[] = data.skill_stack;
  const personalProjects:ProjectType[] = data.project;
  const qualifications:string[] = data.qualification;

  return (
    <Box margin="5">
      <header>
        <Heading as="h2" fontSize="4xl" letterSpacing="wider">{name}</Heading>
        <Flex flexWrap="wrap" alignItems="center" spacing={3} marginY="3" justifyContent="space-between">
          <Box flex="1 1 auto">
          <Text fontWeight="semibold">
           <EmailIcon color="#992214" /> {email}
          </Text>
          </Box>
          <Box flex="1 1 auto">
          <Text fontWeight="semibold">
           <PhoneIcon color="#992214" /> { phone }
          </Text>
          </Box>
          <Box flex="1 1 auto">
          <Text fontWeight="semibold">
           <LinkIcon color="#992214" /> {website}
          </Text>
          </Box>
          <Box flex="1 1 auto">
          <Text fontWeight="semibold">
            <AtSignIcon color="#992214" />{address}
          </Text>
          </Box>
        </Flex>
      </header>
      <Box marginY="7">
        <H3 content="サマリー"></H3>
        <Text>{summary}</Text>
      </Box>
      <Flex>
        <VStack>
          <Box>
            <H3 content="職歴"></H3>
            <Divider />
            { works.map(({company, position, website, startDate, endDate, summary, highlights}) => (
              <>
                <H4 content={company}></H4>
                <Text fontSize="md">{position}</Text>
                <TimeSpan startDate={startDate} endDate={endDate} />
                <Text marginY="3">{summary}</Text>
                <UnorderedList>
                  {highlights.map((highlight) => (
                    <ListItem fontSize="sm">{highlight}</ListItem>
                  ))}
                </UnorderedList>
              </>
            ))}
          </Box>
          <Box>
            <H3 content="学歴"></H3>
            <Divider />
            { educations.map(({institution, area, startDate, endDate}) => (
              <>
                <H4 content={institution}></H4>
                <Text fontSize="md">{area}</Text>
                <TimeSpan startDate={startDate} endDate={endDate} />
              </>
            ))}
          </Box>
          <Box>
            <H3 content="プログラミング言語"></H3>
            <Divider />
            { programingLanguages.map((language) => (
              <Tag margin="0.5" >
                { language }
              </Tag>
            ))}
          </Box>
          <Box>
            <H3 content="資格"></H3>
            <Divider />
            { qualifications.map((qualification) => (
              <Tag margin="0.5" >
                { qualification }
              </Tag>
            ))}
          </Box>
        </VStack>
        <Box w="3rem"></Box>
        <VStack>
          <Box>
            <H3 content="プロジェクト"></H3>
            <Divider />
            { personalProjects.map(({heading, summary, highlights}) => (
              <>
                <H4 content={heading}></H4>
                <Text fontSize="md" marginBottom="3">{summary}</Text>
                <UnorderedList>
                  {highlights.map((highlight) => (
                    <ListItem fontSize="sm">{highlight}</ListItem>
                  ))}
                </UnorderedList>
              </>
            ))}
          </Box>
          <Box>
            <H3 content="技術スタック"></H3>
            <Divider />
            { techStacks.map((stack) => (
              <Tag margin="0.5" >
                { stack }
              </Tag>
            ))}
          </Box>
          <Box w="100%">
            <H3 content="言語"></H3>
            <Divider />
            { naturalLanguages.map(({language, fluency, points}) => (
              <Flex justifyContent="space-between">
                <Text fontSize="md">{language}</Text>
                <Text>{fluency} <StarRating
                  unit="float"
                  activeColor={"#9AE6B4"}
                  initialRating={points}
                  size={15}
                /></Text>
              </Flex>
            ))}
          </Box>
        </VStack>
      </Flex>
    </Box>
  );
}

export default App;
components.tsx
import { Text, Heading } from "@chakra-ui/react"
import { CalendarIcon } from '@chakra-ui/icons'

export const H3 = ({content}: any) => {
    return <Heading as="h3" fontSize="3xl">{content}</Heading>
}

export const H4 = ({content}: any) => {
    return <Heading as="h4" fontSize="xl" fontWeight="semibold" marginTop="4" marginBottom="1">{content}</Heading>
}


type TimeSpanProps = {
    startDate: string,
    endDate: string
}

export const TimeSpan: React.VFC<TimeSpanProps> = ({startDate, endDate}) => {
    return <Text fontSize="sm"><CalendarIcon color="#992214" /> {startDate}~{endDate}</Text>
}
index.tsx
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { ChakraProvider } from "@chakra-ui/react"
import { extendTheme } from "@chakra-ui/react"

const theme = extendTheme({
  styles: {
    global: {
      svg: {
        display: "unset",
      }
    },
  },
  components: {
    Heading: {
      baseStyle: {
        color: "#159957"
      },
    },
    Text: {
      baseStyle: {
        color: "#384347"
      }
    }
  },
})

ReactDOM.render(
  <ChakraProvider theme={theme}>
    <App />
  </ChakraProvider>,
  document.getElementById('root')
);
types.ts
export type WorkType = {
  company: string,
  position: string,
  website: string,
  startDate: string,
  endDate: string,
  summary: string,
  highlights: string[]
}

export type EducationType = {
  institution: string,
  area: string,
  startDate: string,
  endDate: string,
}

export type ProjectType = {
  heading: string,
  summary: string,
  highlights: string[]
}

export type NaturalLanguageType = {
  language: string,
  fluency: string,
  points: number,
}

ちなみに

React で PDF が作れるライブラリや、React ファイルを PDF に変換できるライブラリがありますが、今回は採用しませんでした。

前者は、ふつうに React でページをつくって PDF で出力すれば良いので、今回に限っては学習コストをかけるに値しないと判断したためです。

後者は、コマンドラインで PDF に変換できるのは魅力的ですが、履歴書を PDF に変換する機会はそう頻繁にはなかろうため、ページから手動で PDF に変換するだけで足りるだろうと考えたためです。

最後に

履歴書に足りない項目や経験等ありましたら、ご指摘いただけますと幸いです。

Discussion