Skip to content

Commit 28dd7cb

Browse files
feat: adding new full view of the card that is not a print view
1 parent e8de7ca commit 28dd7cb

18 files changed

Lines changed: 1060 additions & 62 deletions

package-lock.json

Lines changed: 746 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@astrojs/ts-plugin": "^1.10.4",
2020
"@nanostores/persistent": "github:nanostores/persistent",
2121
"@nanostores/react": "^1.0.0",
22+
"@react-three/drei": "^10.7.6",
2223
"astro": "^5.13.7",
2324
"canvg": "^4.0.3",
2425
"clsx": "^2.1.1",
@@ -27,6 +28,7 @@
2728
"qr": "^0.5.1",
2829
"react": "^19.1.1",
2930
"react-dom": "^19.1.1",
31+
"react-now-hook": "^0.0.2",
3032
"zod": "^3.25.76"
3133
},
3234
"prettier": {

src/components/Logo.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
export const Logo = () => (
1+
export type LogoProps = {
2+
src?: string;
3+
};
4+
export const Logo = ({ src }: LogoProps) => (
25
<a
36
href="https://oktech.jp"
47
target="_blank"
58
title="Go to the OKTech.jp homepage"
69
>
710
<img
811
alt="OKTech Logo"
9-
src="https://public.oktech.jp/images/logo-and-design/OKTech-logo-onlight.svg"
12+
src={
13+
src ??
14+
"https://public.oktech.jp/images/logo-and-design/OKTech-logo-onlight.svg"
15+
}
1016
data-variant="winter"
1117
style={{ height: "2em", padding: "0.5em" }}
1218
/>

src/components/docs/card/CardDisplayBack.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { PermanentMarker } from "@/components/fonts/PermanentMarker";
55
import { EmbeddedSVGImage } from "@/components/utils/EmbeddedSVGImage";
66
import { useSvgSize } from "@/hooks/useSvgSize";
77
import { ColorInfo } from "@/components/docs/card/ColorInfo";
8-
import type { DocPageView } from "@/components/safeDoc-react/DocView";
8+
import type { DocFullView } from "@/components/safeDoc-react/DocView";
99

10-
export const CardDisplayBack: DocPageView<CardV1Type> = ({
10+
export const CardDisplayBack: DocFullView<CardV1Type> = ({
1111
data: json,
1212
showMargins,
1313
onReady,

src/components/docs/card/CardDisplayFront.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import clsx from "clsx";
2-
import type { Ref } from "react";
2+
import { useMemo, type Ref } from "react";
33
import { NotoSansJP } from "@/components/fonts/NotoSansJP";
44
import { CARD_SIZE, BusinessCardSvg } from "@/components/docs/BusinessCardSvg";
55
import { useSvgSize } from "@/hooks/useSvgSize";
@@ -8,7 +8,7 @@ import { useQRCode } from "@/hooks/useQrCode";
88
import { ICON_RATIOS } from "@/docs/card/icon-ratios";
99
import { ICON_WHITE_BG } from "@/docs/card/icon-white-bg";
1010
import type { AllIconTypes, CardV1Type } from "@/docs/card";
11-
import type { DocPageView } from "@/components/safeDoc-react/DocView";
11+
import type { DocFullView } from "@/components/safeDoc-react/DocView";
1212
import { NotoSansArabic } from "@/components/fonts/NotoSansArabic";
1313
import { TextWithFont } from "@/components/fonts/TextWithFont";
1414
import { useOnAllReady } from "@/hooks/useOnAllReady";
@@ -59,7 +59,7 @@ const Big = ({ kana, text, className, onReady, ref }: BigProps) => {
5959
);
6060
};
6161

62-
export const CardDisplayFront: DocPageView<CardV1Type> = ({
62+
export const CardDisplayFront: DocFullView<CardV1Type> = ({
6363
type,
6464
docKey,
6565
data: json,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.fullcard--drei-loader {
2+
position: absolute;
3+
container-type: size;
4+
top: -10000px;
5+
left: -10000px;
6+
}
7+
.fullcard {
8+
display: grid;
9+
grid-template-columns: [full-start] 100px [borderless-start] 3em [content-start] 1fr [content-end] 3em [borderless-end] 100px [full-end];
10+
}
11+
.fullcard > * {
12+
grid-column: content;
13+
}
14+
.fullcard > .full {
15+
grid-column: full;
16+
}
17+
.fullcard footer {
18+
width: 100%;
19+
position: fixed;
20+
bottom: 0;
21+
padding: 1em 0;
22+
background: rgba(255, 255, 255, 0.75);
23+
backdrop-filter: blur(0.15em);
24+
div {
25+
display: flex;
26+
gap: 0.4em;
27+
}
28+
.icon {
29+
border: 1px solid transparent;
30+
}
31+
.border {
32+
border: 1px solid #acacac;
33+
}
34+
}
35+
.fullcard footer:before {
36+
content: "";
37+
}
38+
.fullcard .text {
39+
font-family: "Noto Sans JP";
40+
padding-top: 2em;
41+
}
42+
.fullcard .name {
43+
font-size: 1.5em;
44+
}
45+
.fullcard .arabic {
46+
font-family: "Noto Sans Arabic";
47+
rt {
48+
margin-bottom: 0.2em;
49+
}
50+
}
51+
.fullcard {
52+
h1 {
53+
font-family: "Permanent Marker";
54+
font-size: 3em;
55+
rt {
56+
font-family: "Shippori Antique B1";
57+
font-size: 0.3em;
58+
}
59+
}
60+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import type { DocFullViewProps } from "@/components/safeDoc-react/DocView";
2+
import { DEFAULT_COLOR, type CardV1 } from "@/docs/card";
3+
import { CardDisplayBack } from "./CardDisplayBack";
4+
import { useOnAllReady } from "@/hooks/useOnAllReady";
5+
import { CardDisplayFront } from "./CardDisplayFront";
6+
import { useRef, useState } from "react";
7+
import { svgToImage } from "@/utils/print";
8+
import { Canvas, useFrame } from "@react-three/fiber";
9+
import { Image } from "@react-three/drei";
10+
import { CARD_SIZE } from "../BusinessCardSvg";
11+
import * as THREE from "three";
12+
import { ColorInfo } from "./ColorInfo";
13+
import { Logo } from "@/components/Logo";
14+
import "./CardFullView.css";
15+
import { ICON_WHITE_BG } from "@/docs/card/icon-white-bg";
16+
import { ICON_RATIOS } from "@/docs/card/icon-ratios";
17+
import clsx from "clsx";
18+
import { isArabic } from "@/components/utils/isArabic";
19+
20+
export type CardFullViewProps = DocFullViewProps<typeof CardV1>;
21+
const scale = [CARD_SIZE.normal.width / 200, CARD_SIZE.normal.height / 200] as [
22+
number,
23+
number,
24+
];
25+
26+
function DreiView({ front, back }: { front: string; back: string }) {
27+
const fRef = useRef<THREE.Mesh>(null);
28+
const bRef = useRef<THREE.Mesh>(null);
29+
useFrame(() => {
30+
if (!fRef.current || !bRef.current) return;
31+
const angleD = Math.round((Date.now() / 12000) * 1000) % 1000;
32+
const angle = (angleD / 1000) * 2 * Math.PI;
33+
bRef.current.setRotationFromEuler(new THREE.Euler(0, angle, 0));
34+
fRef.current.setRotationFromEuler(new THREE.Euler(0, angle + Math.PI, 0));
35+
});
36+
return (
37+
<>
38+
<Image ref={fRef} url={front} scale={scale} />
39+
<Image ref={bRef} url={back} scale={scale} />
40+
</>
41+
);
42+
}
43+
44+
function RotatingCardView(props: CardFullViewProps) {
45+
const front = useRef<SVGSVGElement>(null);
46+
const back = useRef<SVGSVGElement>(null);
47+
const [frontUrl, setFrontUrl] = useState<string>();
48+
const [backUrl, setBackUrl] = useState<string>();
49+
const [useFrontReady, useBackReady] = useOnAllReady(() => {
50+
props.onReady?.();
51+
Promise.all([
52+
svgToImage(front.current!).then((img) =>
53+
setFrontUrl(URL.createObjectURL(img)),
54+
),
55+
svgToImage(back.current!).then((img) =>
56+
setBackUrl(URL.createObjectURL(img)),
57+
),
58+
]).catch((error) => {
59+
console.log(error);
60+
});
61+
}, 2);
62+
return (
63+
<>
64+
<div
65+
className="fullcard--drei-loader"
66+
style={{ width: CARD_SIZE.normal.width }}
67+
>
68+
{frontUrl ? null : (
69+
<CardDisplayFront ref={front} {...props} onReady={useFrontReady} />
70+
)}
71+
{backUrl ? null : (
72+
<CardDisplayBack ref={back} {...props} onReady={useBackReady} />
73+
)}
74+
</div>
75+
{frontUrl && backUrl ? (
76+
<Canvas style={{ height: "50vh", width: "100cqw", gridColumn: "full" }}>
77+
<DreiView front={frontUrl} back={backUrl} />
78+
</Canvas>
79+
) : null}
80+
</>
81+
);
82+
}
83+
84+
const CardDesignView = ({ data }: CardFullViewProps) => {
85+
const colorInfo = ColorInfo[data.color] ?? ColorInfo[DEFAULT_COLOR];
86+
const colorStyle = { backgroundColor: colorInfo.bg, color: colorInfo.fg };
87+
return (
88+
<>
89+
<header style={colorStyle} className="full">
90+
<Logo src={colorInfo.logo} />
91+
</header>
92+
<section style={colorStyle} className="full fullcard">
93+
<menu></menu>
94+
<h1>
95+
<ruby>
96+
{data.callname}
97+
<rt>{data.callname_kana}</rt>
98+
</ruby>
99+
</h1>
100+
</section>
101+
<section className="text">
102+
<div
103+
className={clsx("name", {
104+
arabic: isArabic(data.surname) || isArabic(data.firstname),
105+
})}
106+
>
107+
<strong>
108+
<ruby>
109+
{data.surname}
110+
<rt>{data.surname_kana}</rt>
111+
</ruby>
112+
</strong>{" "}
113+
<ruby>
114+
{data.firstname}
115+
<rt>{data.firstname_kana}</rt>
116+
</ruby>
117+
</div>
118+
<div className="subtitle">{data.subtitle}</div>
119+
{data.email ? (
120+
<div className="email">
121+
<a href={`mailto:${data.email}`}>{data.email}</a>
122+
</div>
123+
) : null}
124+
{data.url ? (
125+
<div className="url">
126+
<a href={data.url}>{data.url}</a>
127+
</div>
128+
) : null}
129+
</section>
130+
</>
131+
);
132+
};
133+
134+
export const CardFullView = (props: CardFullViewProps) => {
135+
const icons = [
136+
props.data.bottom1,
137+
props.data.bottom2,
138+
props.data.bottom3,
139+
props.data.bottom4,
140+
props.data.bottom5,
141+
].filter(Boolean) as string[];
142+
return (
143+
<div className="fullcard">
144+
<CardDesignView {...props} />
145+
<RotatingCardView {...props} />
146+
{icons.length > 0 || props.data.description ? (
147+
<footer className="fullcard">
148+
<div>
149+
{icons.map((value) => {
150+
const border =
151+
ICON_WHITE_BG[value as keyof typeof ICON_WHITE_BG] ?? false;
152+
const height = 20;
153+
const width =
154+
ICON_RATIOS[value as keyof typeof ICON_RATIOS] * height;
155+
return (
156+
<img
157+
key={value}
158+
className={clsx({ border, icon: true })}
159+
src={`/svg/${value}.svg`}
160+
width={width}
161+
height={height}
162+
/>
163+
);
164+
})}
165+
{props.data.description}
166+
</div>
167+
</footer>
168+
) : null}
169+
</div>
170+
);
171+
};
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import type { CardV1 } from "@/docs/card";
2-
import type { DocView, DocViewProps } from "@/components/safeDoc-react/DocView";
2+
import type {
3+
DocPageView,
4+
DocPageViewProps,
5+
} from "@/components/safeDoc-react/DocView";
36
import { CardDisplayFront } from "@/components/docs/card/CardDisplayFront";
47
import { CardDisplayBack } from "@/components/docs/card/CardDisplayBack";
58

6-
export type CardViewProps = DocViewProps<typeof CardV1>;
9+
export type CardPageViewProps = DocPageViewProps<typeof CardV1>;
710

8-
export const CardView: DocView<typeof CardV1> = ({
11+
export const CardPageView: DocPageView<typeof CardV1> = ({
912
page,
1013
...props
11-
}: CardViewProps) => {
14+
}: CardPageViewProps) => {
1215
if (page === "front") return <CardDisplayFront {...props} />;
1316
if (page === "back") return <CardDisplayBack {...props} />;
1417

src/components/safeDoc-react/DocEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const DocEditor = ({ docKey, setup }: DocEditorProps) => {
4242
docState.saveDraft(type, formToJSON(formRef.current!));
4343
};
4444
const TypeForm = setup.forms.get(type)!;
45-
const TypeView = setup.views.get(type)!;
45+
const TypeView = setup.pageViews.get(type)!;
4646
const pages = activeDoc.type.getPages(activeDoc.data);
4747
return (
4848
<div className="sd--editor">

src/components/safeDoc-react/DocPrinter.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export function DocPrinter({ docKey, setup }: DocPrinterProps) {
5050
const draft = ready?.draft ? { isDraft: true, ...ready.draft } : undefined;
5151
const published = ready?.doc ? { isDraft: false, ...ready.doc } : undefined;
5252
const selected = confirmDraft ? (draft ?? published) : published;
53-
const TypeView = selected?.type ? setup.views.get(selected.type) : undefined;
53+
const TypeView = selected?.type
54+
? setup.pageViews.get(selected.type)
55+
: undefined;
5456
const PrintView = selected?.type
5557
? setup.prints.get(selected.type)
5658
: undefined;
@@ -223,6 +225,7 @@ export function DocPrinter({ docKey, setup }: DocPrinterProps) {
223225
return (
224226
<div key={page.id} className="print--page">
225227
<TypeView
228+
onReady={() => {}}
226229
page={page.id}
227230
ref={(content) => {
228231
if (content) {

0 commit comments

Comments
 (0)