repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
certificates
stars
latest
clone command
git clone gitlawb://did:key:z6Mkqhmm...XL9c/certificatesgit clone gitlawb://did:key:z6Mkqhmm.../certificates019974a8sync from playground15h ago| #1 | import React, { useState, useEffect, Component, type ReactNode, type ErrorInfo } from "react"; |
| #2 | import type { CertificateProps } from "../types"; |
| #3 | |
| #4 | declare global { |
| #5 | interface Window { |
| #6 | Babel?: { |
| #7 | transform: (code: string, opts: Record<string, unknown>) => { code: string }; |
| #8 | }; |
| #9 | } |
| #10 | } |
| #11 | |
| #12 | // ── Error Boundary ────────────────────────────────────────────────── |
| #13 | // Catches render-time crashes in compiled templates so a bad template |
| #14 | // doesn't take down the entire React tree (white-screen). |
| #15 | |
| #16 | interface BoundaryState { |
| #17 | hasError: boolean; |
| #18 | message: string; |
| #19 | } |
| #20 | |
| #21 | class TemplateErrorBoundary extends Component< |
| #22 | { children: ReactNode; onError?: (msg: string) => void }, |
| #23 | BoundaryState |
| #24 | > { |
| #25 | state: BoundaryState = { hasError: false, message: "" }; |
| #26 | |
| #27 | static getDerivedStateFromError(err: unknown): Partial<BoundaryState> { |
| #28 | return { |
| #29 | hasError: true, |
| #30 | message: err instanceof Error ? err.message : String(err), |
| #31 | }; |
| #32 | } |
| #33 | |
| #34 | componentDidCatch(err: unknown, info: ErrorInfo) { |
| #35 | console.error("[TemplateErrorBoundary]", err, info.componentStack); |
| #36 | this.props.onError?.(this.state.message); |
| #37 | } |
| #38 | |
| #39 | render() { |
| #40 | if (this.state.hasError) { |
| #41 | return <ErrorDisplay message={this.state.message} />; |
| #42 | } |
| #43 | return this.props.children; |
| #44 | } |
| #45 | } |
| #46 | |
| #47 | // ── Babel compiler helpers ────────────────────────────────────────── |
| #48 | |
| #49 | function buildComponentSource(userCode: string): string { |
| #50 | return `(function(cert) { |
| #51 | ${userCode} |
| #52 | })`; |
| #53 | } |
| #54 | |
| #55 | function compileCode(code: string): React.FC<CertificateProps> { |
| #56 | if (!window.Babel) { |
| #57 | throw new Error("Babel compiler not loaded. Please refresh the page."); |
| #58 | } |
| #59 | const src = buildComponentSource(code); |
| #60 | console.log("[compileCode] source:", src); |
| #61 | const result = window.Babel.transform(src, { |
| #62 | presets: ["react"], |
| #63 | filename: "custom-template.tsx", |
| #64 | }); |
| #65 | // Babel standalone may prepend "use strict"; which would short-circuit |
| #66 | // the return statement. Strip it so the actual function is returned. |
| #67 | const cleaned = result.code.replace(/^"use strict";\s*/, ""); |
| #68 | console.log("[compileCode] compiled:", cleaned); |
| #69 | // eslint-disable-next-line no-new-func |
| #70 | const fn = new Function("React", `return ${cleaned}`)(React); |
| #71 | if (typeof fn !== "function") { |
| #72 | throw new Error( |
| #73 | `Compiled template did not produce a function (got ${typeof fn}). ` + |
| #74 | `Babel output:\n${cleaned}` |
| #75 | ); |
| #76 | } |
| #77 | return (props: CertificateProps) => fn(props); |
| #78 | } |
| #79 | |
| #80 | // ── Public components ─────────────────────────────────────────────── |
| #81 | |
| #82 | /** Renders a custom template from localStorage by templateId */ |
| #83 | export default function CustomTemplateRenderer(cert: CertificateProps) { |
| #84 | const [TemplateComp, setTemplateComp] = useState<React.FC<CertificateProps> | null>(null); |
| #85 | const [error, setError] = useState<string | null>(null); |
| #86 | |
| #87 | useEffect(() => { |
| #88 | try { |
| #89 | const raw = localStorage.getItem("cert-custom-templates"); |
| #90 | if (!raw) { |
| #91 | setError("Custom template not found"); |
| #92 | return; |
| #93 | } |
| #94 | const templates = JSON.parse(raw) as Array<{ id: string; code: string }>; |
| #95 | const match = templates.find((t) => t.id === cert.templateId); |
| #96 | if (!match) { |
| #97 | setError("Custom template not found"); |
| #98 | return; |
| #99 | } |
| #100 | // Compile BEFORE passing to setState so the try/catch actually catches it |
| #101 | const compiled = compileCode(match.code); |
| #102 | setTemplateComp(() => compiled); |
| #103 | setError(null); |
| #104 | } catch (err) { |
| #105 | console.error("[CustomTemplateRenderer]", err); |
| #106 | setError(err instanceof Error ? err.message : String(err)); |
| #107 | setTemplateComp(null); |
| #108 | } |
| #109 | }, [cert.templateId]); |
| #110 | |
| #111 | if (error) { |
| #112 | return <ErrorDisplay message={error} />; |
| #113 | } |
| #114 | |
| #115 | if (!TemplateComp) { |
| #116 | return <LoadingDisplay />; |
| #117 | } |
| #118 | |
| #119 | return ( |
| #120 | <TemplateErrorBoundary> |
| #121 | <TemplateComp {...cert} /> |
| #122 | </TemplateErrorBoundary> |
| #123 | ); |
| #124 | } |
| #125 | |
| #126 | /** Renders a custom template from raw code (used for editor preview) */ |
| #127 | export function CustomTemplatePreview({ code, cert }: { code: string; cert: CertificateProps }) { |
| #128 | const [TemplateComp, setTemplateComp] = useState<React.FC<CertificateProps> | null>(null); |
| #129 | const [error, setError] = useState<string | null>(null); |
| #130 | |
| #131 | useEffect(() => { |
| #132 | try { |
| #133 | // Compile BEFORE passing to setState so the try/catch actually catches it |
| #134 | const compiled = compileCode(code); |
| #135 | setTemplateComp(() => compiled); |
| #136 | setError(null); |
| #137 | } catch (err) { |
| #138 | console.error("[CustomTemplatePreview]", err); |
| #139 | setError(err instanceof Error ? err.message : String(err)); |
| #140 | setTemplateComp(null); |
| #141 | } |
| #142 | }, [code]); |
| #143 | |
| #144 | if (error) { |
| #145 | return <ErrorDisplay message={error} />; |
| #146 | } |
| #147 | |
| #148 | if (!TemplateComp) { |
| #149 | return <LoadingDisplay />; |
| #150 | } |
| #151 | |
| #152 | return ( |
| #153 | <TemplateErrorBoundary> |
| #154 | <TemplateComp {...cert} /> |
| #155 | </TemplateErrorBoundary> |
| #156 | ); |
| #157 | } |
| #158 | |
| #159 | // ── Display helpers ───────────────────────────────────────────────── |
| #160 | |
| #161 | function ErrorDisplay({ message }: { message: string }) { |
| #162 | return ( |
| #163 | <div |
| #164 | id="cert-render" |
| #165 | style={{ |
| #166 | width: 1056, |
| #167 | height: 816, |
| #168 | background: "#fef2f2", |
| #169 | display: "flex", |
| #170 | alignItems: "center", |
| #171 | justifyContent: "center", |
| #172 | fontFamily: "monospace", |
| #173 | color: "#dc2626", |
| #174 | padding: 40, |
| #175 | boxSizing: "border-box", |
| #176 | }} |
| #177 | > |
| #178 | <div style={{ textAlign: "center" }}> |
| #179 | <p style={{ fontSize: 18, fontWeight: "bold", marginBottom: 12 }}>Template Error</p> |
| #180 | <p style={{ fontSize: 14, whiteSpace: "pre-wrap" }}>{message}</p> |
| #181 | </div> |
| #182 | </div> |
| #183 | ); |
| #184 | } |
| #185 | |
| #186 | function LoadingDisplay() { |
| #187 | return ( |
| #188 | <div |
| #189 | id="cert-render" |
| #190 | style={{ |
| #191 | width: 1056, |
| #192 | height: 816, |
| #193 | background: "#f9fafb", |
| #194 | display: "flex", |
| #195 | alignItems: "center", |
| #196 | justifyContent: "center", |
| #197 | fontFamily: "sans-serif", |
| #198 | color: "#9ca3af", |
| #199 | }} |
| #200 | > |
| #201 | Loading template... |
| #202 | </div> |
| #203 | ); |
| #204 | } |
| #205 |