aboutsummaryrefslogtreecommitdiff
path: root/src/components/preview
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/preview')
-rw-r--r--src/components/preview/__snapshots__/badge.test.tsx.snap21
-rw-r--r--src/components/preview/__snapshots__/card.test.tsx.snap165
-rw-r--r--src/components/preview/badge.test.tsx22
-rw-r--r--src/components/preview/badge.tsx57
-rw-r--r--src/components/preview/card.test.tsx163
-rw-r--r--src/components/preview/card.tsx198
-rw-r--r--src/components/preview/preview.tsx218
7 files changed, 844 insertions, 0 deletions
diff --git a/src/components/preview/__snapshots__/badge.test.tsx.snap b/src/components/preview/__snapshots__/badge.test.tsx.snap
new file mode 100644
index 0000000..9000a91
--- /dev/null
+++ b/src/components/preview/__snapshots__/badge.test.tsx.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Badge renders 1`] = `
+<div
+ class="badge-wrapper"
+ style="height: 25px; background-color: rgb(85, 85, 85); display: flex; margin: 0px 7px;"
+>
+ <p
+ class="badge-label"
+ style="color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px; text-transform: uppercase; padding: 0px 12px 0px 10px; display: flex; align-items: center;"
+ >
+ name1
+ </p>
+ <p
+ class="badge-value"
+ style="background-color: black; color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px -4px 0px -4px; padding: 0px 8px; display: flex; align-items: center;"
+ >
+ value1
+ </p>
+</div>
+`;
diff --git a/src/components/preview/__snapshots__/card.test.tsx.snap b/src/components/preview/__snapshots__/card.test.tsx.snap
new file mode 100644
index 0000000..e5ec993
--- /dev/null
+++ b/src/components/preview/__snapshots__/card.test.tsx.snap
@@ -0,0 +1,165 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Card #1 renders 1`] = `
+<div
+ class="card-wrapper theme-light"
+ style="width: 640px; height: 320px; padding: 10px 30px; font-family: Inter; font-weight: 400; background-color: rgb(255, 255, 255); background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2242%22%20height%3D%2244%22%20viewBox%3D%220%200%2042%2044%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M0%200h42v44H0V0zm1%201h40v20H1V1zM0%2023h20v20H0V23zm22%200h20v20H22V23z%22%20fill%3D%22%23eaeaea%22%20fill-opacity%3D%220.6%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E); background-size: 42px 44px; background-repeat: repeat; color: rgb(0, 0, 0); text-align: center; overflow: hidden; display: flex; flex-direction: column; justify-content: center; align-items: center; transform: scale(2); transform-origin: top left;"
+>
+ <div
+ class="card-logo-wrapper"
+ style="display: flex; justify-content: center; align-items: center; margin-top: 10px;"
+ >
+ <img
+ alt="Logo"
+ height="100"
+ src="data:image/svg+xml,%3Csvg%20fill%3D%22%23181717%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E"
+ style="object-fit: contain;"
+ width="100"
+ />
+ </div>
+ <p
+ class="card-name-wrapper"
+ style="display: flex; align-items: center; margin-top: 15px; margin-bottom: 0px; font-weight: 500; font-size: 40px; line-height: 1.4;"
+ >
+ <span
+ class="card-name-owner"
+ style="display: flex; white-space: nowrap; font-weight: 200;"
+ />
+ <span
+ class="card-name-name"
+ style="display: flex; white-space: nowrap;"
+ >
+ project_name
+ </span>
+ </p>
+</div>
+`;
+
+exports[`Card #2 renders 1`] = `
+<div
+ class="card-wrapper theme-dark"
+ style="width: 640px; height: 320px; padding: 10px 30px; font-family: KoHo; font-weight: 400; background-color: rgb(0, 0, 0); background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2242%22%20height%3D%2244%22%20viewBox%3D%220%200%2042%2044%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M0%200h42v44H0V0zm1%201h40v20H1V1zM0%2023h20v20H0V23zm22%200h20v20H22V23z%22%20fill%3D%22%23eaeaea%22%20fill-opacity%3D%220.2%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E); background-size: 42px 44px; background-repeat: repeat; color: rgb(255, 255, 255); text-align: center; overflow: hidden; display: flex; flex-direction: column; justify-content: center; align-items: center; transform: scale(2); transform-origin: top left;"
+>
+ <div
+ class="card-logo-wrapper"
+ style="display: flex; justify-content: center; align-items: center; margin-top: 10px;"
+ >
+ <img
+ alt="Logo"
+ height="100"
+ src="data:image/gif;base64,R0lGODlhAQABAAAAACw="
+ style="object-fit: contain;"
+ width="100"
+ />
+ <p
+ class="card-logo-divider"
+ style="color: rgb(187, 187, 187); font-size: 30px; margin: 0px 20px; font-family: Jost;"
+ >
+ +
+ </p>
+ <img
+ alt="JavaScript"
+ height="85"
+ src="data:image/svg+xml,%3Csvg%20fill%3D%22%23fff%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EJavaScript%3C%2Ftitle%3E%3Cpath%20d%3D%22M0%200h24v24H0V0zm22.034%2018.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84%201.515-.66.39.12.75.42.976.9%201.034-.676%201.034-.676%201.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71%201.005-1.14%201.291-.811%203.541.569%204.471%201.365%201.02%203.361%201.244%203.616%202.205.24%201.17-.87%201.545-1.966%201.41-.811-.18-1.26-.586-1.755-1.336l-1.83%201.051c.21.48.45.689.81%201.109%201.74%201.756%206.09%201.666%206.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0%201.938-.009%203.864-.009%205.805%200%201.232.063%202.363-.138%202.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825%201.125c.305.63.75%201.172%201.324%201.517.855.51%202.004.675%203.207.405.783-.226%201.458-.691%201.811-1.411.51-.93.402-2.07.397-3.346.012-2.054%200-4.109%200-6.179l.004-.056z%22%2F%3E%3C%2Fsvg%3E"
+ style="object-fit: contain;"
+ width="85"
+ />
+ </div>
+ <p
+ class="card-name-wrapper"
+ style="display: flex; align-items: center; margin-top: 15px; margin-bottom: 0px; font-weight: 500; font-size: 40px; line-height: 1.4;"
+ >
+ <span
+ class="card-name-owner"
+ style="display: flex; white-space: nowrap; font-weight: 200;"
+ >
+ owner/
+ </span>
+ <span
+ class="card-name-name"
+ style="display: flex; white-space: nowrap;"
+ >
+ project_name
+ </span>
+ </p>
+ <p
+ class="card-description-wrapper"
+ style="margin-top: 10px; margin-bottom: 0px; font-size: 17px; line-height: 1.4; max-height: 3em; overflow: hidden; word-break: break-all;"
+ >
+ TEST DESCRIPTION
+ </p>
+ <div
+ class="card-badges-wrapper"
+ style="margin-top: 25px; margin-bottom: 0px; display: flex; flex-direction: row;"
+ >
+ <div
+ class="badge-wrapper"
+ style="height: 25px; background-color: rgb(85, 85, 85); display: flex; margin: 0px 7px;"
+ >
+ <p
+ class="badge-label"
+ style="color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px; text-transform: uppercase; padding: 0px 12px 0px 10px; display: flex; align-items: center;"
+ >
+ stars
+ </p>
+ <p
+ class="badge-value"
+ style="background-color: rgb(223, 179, 23); color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px -4px 0px -4px; padding: 0px 8px; display: flex; align-items: center;"
+ >
+ 1
+ </p>
+ </div>
+ <div
+ class="badge-wrapper"
+ style="height: 25px; background-color: rgb(85, 85, 85); display: flex; margin: 0px 7px;"
+ >
+ <p
+ class="badge-label"
+ style="color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px; text-transform: uppercase; padding: 0px 12px 0px 10px; display: flex; align-items: center;"
+ >
+ forks
+ </p>
+ <p
+ class="badge-value"
+ style="background-color: rgb(151, 202, 0); color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px -4px 0px -4px; padding: 0px 8px; display: flex; align-items: center;"
+ >
+ 2
+ </p>
+ </div>
+ <div
+ class="badge-wrapper"
+ style="height: 25px; background-color: rgb(85, 85, 85); display: flex; margin: 0px 7px;"
+ >
+ <p
+ class="badge-label"
+ style="color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px; text-transform: uppercase; padding: 0px 12px 0px 10px; display: flex; align-items: center;"
+ >
+ issues
+ </p>
+ <p
+ class="badge-value"
+ style="background-color: rgb(0, 126, 198); color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px -4px 0px -4px; padding: 0px 8px; display: flex; align-items: center;"
+ >
+ 3
+ </p>
+ </div>
+ <div
+ class="badge-wrapper"
+ style="height: 25px; background-color: rgb(85, 85, 85); display: flex; margin: 0px 7px;"
+ >
+ <p
+ class="badge-label"
+ style="color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px; text-transform: uppercase; padding: 0px 12px 0px 10px; display: flex; align-items: center;"
+ >
+ pulls
+ </p>
+ <p
+ class="badge-value"
+ style="background-color: rgb(254, 125, 55); color: rgb(255, 255, 255); font-family: Jost; font-size: 11px; height: 100%; letter-spacing: 1px; margin: 0px -4px 0px -4px; padding: 0px 8px; display: flex; align-items: center;"
+ >
+ 4
+ </p>
+ </div>
+ </div>
+</div>
+`;
diff --git a/src/components/preview/badge.test.tsx b/src/components/preview/badge.test.tsx
new file mode 100644
index 0000000..fea753d
--- /dev/null
+++ b/src/components/preview/badge.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@testing-library/react'
+
+import Badge from './badge'
+
+test('Badge renders', () => {
+ const { container } = render(
+ <Badge color="black" name="name1" value="value1" />
+ )
+ const badge = container.firstElementChild!
+
+ expect(badge).toMatchSnapshot()
+ expect(badge.classList.contains('badge-wrapper')).toBe(true)
+ expect(
+ badge.querySelector<HTMLElement>('.badge-label')?.textContent
+ ).toStrictEqual('name1')
+ expect(
+ badge.querySelector<HTMLElement>('.badge-value')?.textContent
+ ).toStrictEqual('value1')
+ expect(
+ badge.querySelector<HTMLElement>('.badge-value')?.style.backgroundColor
+ ).toStrictEqual('black')
+})
diff --git a/src/components/preview/badge.tsx b/src/components/preview/badge.tsx
new file mode 100644
index 0000000..4ccb876
--- /dev/null
+++ b/src/components/preview/badge.tsx
@@ -0,0 +1,57 @@
+import React from 'react'
+
+type BadgeConfig = {
+ name: string
+ value: string
+ color: string
+}
+
+const Badge: React.FC<BadgeConfig> = (config) => {
+ return (
+ <div
+ className="badge-wrapper"
+ style={{
+ height: 25,
+ backgroundColor: '#555',
+ display: 'flex',
+ margin: '0 7px'
+ }}>
+ <p
+ className="badge-label"
+ style={{
+ color: '#fff',
+ fontFamily: 'Jost',
+ fontSize: 11,
+ height: '100%',
+ letterSpacing: 1,
+ margin: 0,
+ textTransform: 'uppercase',
+ padding: '0 12px 0 10px',
+ display: 'flex',
+ alignItems: 'center'
+ }}>
+ {config.name}
+ </p>
+ <p
+ className="badge-value"
+ style={{
+ backgroundColor: config.color,
+ color: '#fff',
+ fontFamily: 'Jost',
+ fontSize: 11,
+ height: '100%',
+ letterSpacing: 1,
+ margin: 0,
+ padding: '0 8px',
+ display: 'flex',
+ alignItems: 'center',
+ marginLeft: -4,
+ marginRight: -4
+ }}>
+ {config.value}
+ </p>
+ </div>
+ )
+}
+
+export default Badge
diff --git a/src/components/preview/card.test.tsx b/src/components/preview/card.test.tsx
new file mode 100644
index 0000000..8b4b945
--- /dev/null
+++ b/src/components/preview/card.test.tsx
@@ -0,0 +1,163 @@
+/* eslint-disable jest/no-conditional-expect */
+import { render } from '@testing-library/react'
+
+import Card from './card'
+
+import Configuration, {
+ Font,
+ Pattern,
+ Theme
+} from '../../../common/types/configType'
+
+test('Card #1 renders', () => {
+ const config: Configuration = {
+ font: Font.inter,
+ logo: '',
+ name: {
+ value: 'project_name',
+ state: true
+ },
+ pattern: Pattern.brickWall,
+ theme: Theme.light
+ }
+
+ const { container } = render(<Card {...config} />)
+
+ const cardWrapper = container.firstElementChild! as HTMLDivElement
+ expect(cardWrapper).toMatchSnapshot()
+
+ expect(cardWrapper).toBeTruthy()
+ expect(cardWrapper.classList.contains('card-wrapper')).toBe(true)
+ expect(cardWrapper.style.fontFamily).toStrictEqual(config.font)
+ expect(
+ cardWrapper.classList.contains(`theme-${config.theme.toLowerCase()}`)
+ ).toBe(true)
+ expect(cardWrapper.querySelectorAll('.card-logo-wrapper img').length).toBe(1)
+ expect(
+ cardWrapper.querySelectorAll<HTMLImageElement>(
+ '.card-logo-wrapper img'
+ )?.[0]?.alt
+ ).toBe('Logo')
+ expect(cardWrapper.querySelectorAll('.card-logo-divider').length).toBe(0)
+ expect(
+ cardWrapper.querySelector('.card-name-name')?.textContent
+ ).toStrictEqual(config.name?.value)
+ expect(cardWrapper.querySelector('.card-description-wrapper')).toBeFalsy()
+ expect(cardWrapper.querySelectorAll('.card-badges-wrapper').length).toBe(0)
+})
+
+test('Card #2 renders', () => {
+ const config: Configuration = {
+ font: Font.koHo,
+ logo: 'data:image/gif;base64,R0lGODlhAQABAAAAACw=',
+ name: {
+ value: 'project_name',
+ state: true
+ },
+ pattern: Pattern.brickWall,
+ theme: Theme.dark,
+ description: {
+ value: 'TEST DESCRIPTION',
+ state: true
+ },
+ owner: {
+ value: 'owner',
+ state: true
+ },
+ language: {
+ value: 'JavaScript',
+ state: true
+ },
+ language2: {
+ value: 'TypeScript',
+ state: true
+ },
+ stargazers: {
+ value: 1,
+ state: true
+ },
+ forks: {
+ value: 2,
+ state: true
+ },
+ issues: {
+ value: 3,
+ state: true
+ },
+ pulls: {
+ value: 4,
+ state: true
+ }
+ }
+
+ const { container } = render(<Card {...config} />)
+
+ const cardWrapper = container.firstElementChild! as HTMLDivElement
+ expect(cardWrapper).toMatchSnapshot()
+
+ expect(cardWrapper).toBeTruthy()
+ expect(cardWrapper.classList.contains('card-wrapper')).toBe(true)
+ expect(cardWrapper.style.fontFamily).toStrictEqual(config.font)
+ expect(
+ cardWrapper.classList.contains(`theme-${config.theme.toLowerCase()}`)
+ ).toBe(true)
+ expect(
+ cardWrapper.querySelector('.card-name-name')?.textContent
+ ).toStrictEqual(config.name?.value)
+ expect(cardWrapper.querySelectorAll('.card-logo-wrapper img').length).toBe(2)
+ expect(
+ cardWrapper.querySelectorAll<HTMLImageElement>(
+ '.card-logo-wrapper img'
+ )?.[0].src
+ ).toBe(config.logo)
+ expect(
+ cardWrapper.querySelectorAll<HTMLImageElement>(
+ '.card-logo-wrapper img'
+ )?.[0]?.alt
+ ).toBe('Logo')
+ expect(cardWrapper.querySelectorAll('.card-logo-divider').length).toBe(1)
+ expect(
+ cardWrapper.querySelectorAll<HTMLImageElement>(
+ '.card-logo-wrapper img'
+ )?.[1]?.alt
+ ).toBe('JavaScript')
+ expect(
+ cardWrapper.querySelector('.card-description-wrapper')?.textContent
+ ).toStrictEqual(config.description?.value)
+ expect(cardWrapper.querySelectorAll('.card-badges-wrapper').length).toBe(1)
+ expect(cardWrapper.querySelectorAll('.card-badges-wrapper > *').length).toBe(
+ 4
+ )
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[0]
+ ?.firstElementChild?.textContent
+ ).toStrictEqual('stars')
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[0]
+ ?.lastElementChild?.textContent
+ ).toStrictEqual(`${config.stargazers?.value}`)
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[1]
+ ?.firstElementChild?.textContent
+ ).toStrictEqual('forks')
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[1]
+ ?.lastElementChild?.textContent
+ ).toStrictEqual(`${config.forks?.value}`)
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[2]
+ ?.firstElementChild?.textContent
+ ).toStrictEqual('issues')
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[2]
+ ?.lastElementChild?.textContent
+ ).toStrictEqual(`${config.issues?.value}`)
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[3]
+ ?.firstElementChild?.textContent
+ ).toStrictEqual('pulls')
+ expect(
+ cardWrapper.querySelectorAll('.card-badges-wrapper > *')[3]
+ ?.lastElementChild?.textContent
+ ).toStrictEqual(`${config.pulls?.value}`)
+})
diff --git a/src/components/preview/card.tsx b/src/components/preview/card.tsx
new file mode 100644
index 0000000..a5ddf5c
--- /dev/null
+++ b/src/components/preview/card.tsx
@@ -0,0 +1,198 @@
+import Badge from './badge'
+
+import Configuration from '../../../common/types/configType'
+
+import { getHeroPattern, getSimpleIconsImageURI } from '../../../common/helpers'
+
+const Card = (config: Configuration) => {
+ const backgroundPatternStyles = getHeroPattern(config.pattern, config.theme)
+
+ const languageIconImageURI =
+ config.language?.state &&
+ getSimpleIconsImageURI(config.language.value, config.theme)
+
+ const language2IconImageURI =
+ config.language2?.state &&
+ getSimpleIconsImageURI(config.language2.value, config.theme)
+
+ const displayName = [
+ config.owner?.state && config.owner?.value,
+ config.name?.state && config.name?.value
+ ]
+ .filter((value) => typeof value === 'string')
+ .join('/')
+ const nameLength = displayName.length
+ const nameFontSize =
+ nameLength > 55
+ ? '17px'
+ : nameLength > 45
+ ? '20px'
+ : nameLength > 35
+ ? '24px'
+ : nameLength > 25
+ ? '30px'
+ : '40px'
+
+ return (
+ <div
+ className={`card-wrapper theme-${config.theme.toLowerCase()}`}
+ style={{
+ width: 640,
+ height: 320,
+ padding: '10px 30px',
+ fontFamily: config.font,
+ fontWeight: 400,
+ ...backgroundPatternStyles,
+ color: config.theme.match(/dark/i) ? '#fff' : '#000',
+ textAlign: 'center',
+ overflow: 'hidden',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ transform: 'scale(2)',
+ transformOrigin: 'top left'
+ }}>
+ {/* Logo */}
+ <div
+ className="card-logo-wrapper"
+ style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: 10
+ }}>
+ {languageIconImageURI && (
+ <img
+ src={languageIconImageURI}
+ alt={config?.language?.value}
+ width={85}
+ height={85}
+ style={{
+ objectFit: 'contain'
+ }}
+ />
+ )}
+ {language2IconImageURI && (
+ <p
+ className="card-logo-divider"
+ style={{
+ color: '#bbb',
+ fontSize: 30,
+ margin: '0 20px',
+ fontFamily: 'Jost'
+ }}>
+ +
+ </p>
+ )}
+ {language2IconImageURI && (
+ <img
+ src={language2IconImageURI}
+ alt={config?.language2?.value}
+ width={85}
+ height={85}
+ style={{
+ objectFit: 'contain'
+ }}
+ />
+ )}
+ </div>
+
+ {/* Name */}
+ <p
+ className="card-name-wrapper"
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ marginTop: 15,
+ marginBottom: 0,
+ fontWeight: 500,
+ fontSize: nameFontSize,
+ lineHeight: 1.4
+ }}>
+ <span
+ className="card-name-owner"
+ style={{
+ display: 'flex',
+ whiteSpace: 'nowrap',
+ fontWeight: 200
+ }}>
+ {config.owner?.state
+ ? `${config.owner.value}${config.name?.state ? '/' : ''}`
+ : ''}
+ </span>
+ <span
+ className="card-name-name"
+ style={{
+ display: 'flex',
+ whiteSpace: 'nowrap'
+ }}>
+ {config.name?.state ? `${config.name.value}` : ''}
+ </span>
+ </p>
+
+ {/* Description */}
+ {config.description?.state && (
+ <p
+ className="card-description-wrapper"
+ style={{
+ marginTop: 10,
+ marginBottom: 0,
+ fontSize: 17,
+ lineHeight: 1.4,
+ maxHeight: '3em',
+ overflow: 'hidden',
+ wordBreak: 'break-all'
+ }}>
+ {config.description.value}
+ </p>
+ )}
+
+ {/* Badges */}
+ {(config.stargazers?.state ||
+ config.forks?.state ||
+ config.issues?.state ||
+ config.pulls?.state) && (
+ <div
+ className="card-badges-wrapper"
+ style={{
+ marginTop: 25,
+ marginBottom: 0,
+ display: 'flex',
+ flexDirection: 'row'
+ }}>
+ {config.stargazers?.state && (
+ <Badge
+ name="stars"
+ value={`${config.stargazers.value}`}
+ color="#dfb317"
+ />
+ )}
+ {config.forks?.state && (
+ <Badge
+ name="forks"
+ value={`${config.forks.value}`}
+ color="#97ca00"
+ />
+ )}
+ {config.issues?.state && (
+ <Badge
+ name="issues"
+ value={`${config.issues.value}`}
+ color="#007ec6"
+ />
+ )}
+ {config.pulls?.state && (
+ <Badge
+ name="pulls"
+ value={`${config.pulls.value}`}
+ color="#fe7d37"
+ />
+ )}
+ </div>
+ )}
+ </div>
+ )
+}
+
+export default Card
diff --git a/src/components/preview/preview.tsx b/src/components/preview/preview.tsx
new file mode 100644
index 0000000..6517696
--- /dev/null
+++ b/src/components/preview/preview.tsx
@@ -0,0 +1,218 @@
+import React, { useContext } from 'react'
+import classnames from 'clsx'
+import Head from 'next/head'
+import Router from 'next/router'
+import { toClipboard } from 'copee'
+import { MdContentCopy, MdDownload } from 'react-icons/md'
+
+import toaster from '../toaster'
+
+import ConfigContext from '../../contexts/ConfigContext'
+import { checkWebpSupport } from '../../../common/helpers'
+import Card from './card'
+
+const getRelativeImageUrl = (format = 'image') => {
+ const [path, query] = Router.asPath.split('?')
+ return `${path}/${format}${query ? `?${query}` : ''}`
+}
+
+const getImageUrl = (format = 'image') => {
+ return `${window.location.protocol}//${
+ window.location.host
+ }${getRelativeImageUrl(format)}`
+}
+
+const copyImageUrl = () => {
+ const screenshotImageUrl = getImageUrl()
+ const success = toClipboard(screenshotImageUrl)
+ if (success) {
+ toaster.success('Copied image url to clipboard')
+ } else {
+ window.open(screenshotImageUrl, '_blank')
+ }
+}
+
+const copyMarkdown = () => {
+ const screenshotImageUrl = getImageUrl()
+ const ogTag = `![${Router.query._name}](${screenshotImageUrl})`
+ const success = toClipboard(ogTag)
+ if (success) {
+ toaster.success('Copied markdown to clipboard')
+ }
+}
+
+const copyImageTag = () => {
+ const screenshotImageUrl = getImageUrl()
+ const ogTag = `<img src="${screenshotImageUrl}" alt="${Router.query._name}" width="640" height="320" />`
+ const success = toClipboard(ogTag)
+ if (success) {
+ toaster.success('Copied image tag to clipboard')
+ }
+}
+
+const copyOpenGraphTags = () => {
+ const ogTag = `
+<meta property="og:image" content="${getImageUrl('png')}" />
+<meta property="og:image:width" content="1280" />
+<meta property="og:image:height" content="640" />
+ `.trim()
+ const success = toClipboard(ogTag)
+ if (success) {
+ toaster.success('Copied open graph tags to clipboard')
+ }
+}
+
+const handleDownload = (fileType: string) => async () => {
+ toaster.info('Downloading...')
+
+ try {
+ const img = new Image()
+ img.onload = () => {
+ const canvas = document.createElement('canvas')
+ canvas.width = 1280
+ canvas.height = 640
+ const context = canvas.getContext('2d')
+ if (context && img) {
+ context.drawImage(img, 0, 0, canvas.width, canvas.height)
+ const dataUrl = canvas.toDataURL(`image/${fileType}`)
+ const link = document.createElement('a')
+ link.download = `${Router.query._name}.${fileType}`
+ link.href = dataUrl
+ link.click()
+ }
+ }
+ img.src = getRelativeImageUrl()
+ } catch (error) {
+ toaster.error('Download failed: Please use a modern browser.')
+ console.error(error)
+ }
+}
+
+const openRelativeURLInNewTab = (fileType: string) => async () => {
+ window.open(getRelativeImageUrl(fileType), '_blank')
+}
+
+const Preview: React.FC = () => {
+ const { config } = useContext(ConfigContext)
+
+ return (
+ <section className="mb-3">
+ <div
+ className={classnames(
+ 'relative cursor-pointer rounded-lg shadow-2xl overflow-hidden',
+ 'w-[320px] h-[160px]',
+ 'min-[384px]:w-[384px] min-[384px]:h-[192px]',
+ 'min-[400px]:w-[400px] min-[400px]:h-[200px]',
+ 'min-[480px]:w-[480px] min-[480px]:h-[240px]',
+ 'min-[640px]:w-[640px] min-[640px]:h-[320px]'
+ )}
+ onClick={copyImageUrl}>
+ <div
+ className={classnames(
+ 'origin-top-left',
+ 'scale-[0.25]',
+ 'min-[384px]:scale-[0.3]',
+ 'min-[400px]:scale-[0.3125]',
+ 'min-[480px]:scale-[0.375]',
+ 'min-[640px]:scale-[0.5]'
+ )}>
+ <Head>
+ <link
+ href={`https://fonts.googleapis.com/css2?family=Jost:wght@400&display=swap`}
+ rel="stylesheet"
+ key="preview-card-fonts-1"
+ />
+ <link
+ href={`https://fonts.googleapis.com/css2?family=${config.font}:wght@200;400;500&display=swap`}
+ rel="stylesheet"
+ key="preview-card-fonts-2"
+ />
+ </Head>
+ <Card {...config} />
+ </div>
+ <img
+ className="absolute top-0 left-0 w-full h-full opacity-0"
+ alt="Card"
+ src={getRelativeImageUrl()}
+ />
+ </div>
+ <div className="card mt-3 mx-auto w-fit bg-base-100 shadow-xl">
+ <div className="card-body px-3 py-2">
+ <div
+ className={classnames('flex justify-center content-center gap-2')}>
+ <div className="dropdown">
+ <label tabIndex={0} className="btn btn-primary btn-sm gap-2">
+ <MdDownload className="w-5 h-5" />
+ Download
+ </label>
+ <ul
+ style={{ width: 'auto' }}
+ tabIndex={0}
+ className="dropdown-content menu menu-compact p-2 shadow bg-base-100 rounded-box w-52">
+ {(checkWebpSupport()
+ ? ['png', 'jpeg', 'webp']
+ : ['png', 'jpeg']
+ ).map((fileType) => (
+ <li key={fileType}>
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ <a
+ className="font-bold gap-2"
+ onClick={handleDownload(fileType)}>
+ <MdDownload className="w-5 h-5" />
+ {`${config.name?.value ?? ''}.${fileType}`}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ <div className="dropdown">
+ <label tabIndex={0} className="btn btn-primary btn-sm gap-2">
+ API URL
+ </label>
+ <ul
+ style={{ width: 'auto' }}
+ tabIndex={0}
+ className="dropdown-content menu menu-compact p-2 shadow bg-base-100 rounded-box w-52">
+ {['svg', 'png'].map((fileType) => (
+ <li key={fileType}>
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ <a
+ className="font-bold gap-2"
+ onClick={openRelativeURLInNewTab(fileType)}
+ target="_blank"
+ rel="noopener noreferrer">
+ {`${config.name?.value ?? ''}.${fileType}`}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ <div className="btn-group">
+ <button className="btn btn-sm gap-2" onClick={copyImageUrl}>
+ <MdContentCopy className="w-4 h-4" />
+ Url
+ </button>
+ <button
+ className="btn btn-sm hidden sm:inline-flex"
+ onClick={copyMarkdown}>
+ Markdown
+ </button>
+ <button
+ className="btn btn-sm hidden sm:inline-flex"
+ onClick={copyImageTag}>
+ {'<img />'}
+ </button>
+ <button
+ className="btn btn-sm gap-2 hidden sm:inline-flex"
+ onClick={copyOpenGraphTags}>
+ Open Graph
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ )
+}
+
+export default Preview