- Preview
- Code
- Data (.dayta)
- Project Settings (config.yaml)
Copy
import React from 'react';
import { Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer';
import { useDaytalog } from 'daytalog';
/** --- Layout constants --- */
const COL_WIDTHS = {
clip: 60,
scene: 40,
shot: 30,
take: 60,
qc: 325,
};
const QC_WIDTHS = {
tc: 100,
category: 60,
desc: 165,
};
const styles = StyleSheet.create({
page: { fontFamily: 'Helvetica', padding: 40, fontSize: 12 },
/* ===== Header ===== */
headerWrap: {
marginBottom: 14,
},
topRow: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
},
titleBlock: { flexGrow: 1, flexShrink: 1, flexBasis: 0, paddingRight: 12 },
projectName: {
fontSize: 10,
letterSpacing: 0.3,
color: '#666',
textTransform: 'uppercase',
marginBottom: 2,
},
title: {
fontSize: 22,
fontWeight: 'bold',
lineHeight: 1.1,
},
metaBlock: {
alignItems: 'flex-end',
minWidth: 180,
},
metaLine: {
fontSize: 9,
color: '#333',
},
divider: {
marginTop: 10,
borderBottomWidth: 1,
borderBottomColor: '#ddd',
},
pageNumber: { fontSize: 7, position: 'absolute', top: 20, right: 20, color: '#666' },
/* ===== Table ===== */
table: { display: 'table', width: '100%', marginTop: 12 },
row: { flexDirection: 'row' },
zebra: { backgroundColor: '#f8f8f8' },
// Main header (clean)
headerCellBox: {
padding: 6,
borderBottomWidth: 0.8,
borderBottomColor: '#bbb',
},
headerTitle: {
fontSize: 7,
fontWeight: 'bold',
textTransform: 'uppercase',
letterSpacing: 0.3,
},
// QC subheaders INSIDE the QC header cell
subHeaderRow: {
flexDirection: 'row',
paddingTop: 4,
marginTop: 2,
borderTopWidth: 0.4,
borderTopColor: '#ddd',
},
subHeaderText: {
fontSize: 6,
fontWeight: 'bold',
color: '#555',
textTransform: 'uppercase',
},
// Body cells
cellView: { borderBottomWidth: 0.2, borderBottomColor: '#ddd', padding: 0 },
cellText: { padding: 6, fontSize: 7, color: '#333' },
// QC rows
subTable: { display: 'table', width: '100%' },
subRow: { flexDirection: 'row' },
subCell: {
padding: 4,
fontSize: 7,
color: '#333',
borderBottomWidth: 0.2,
borderBottomColor: '#eee',
},
});
const QCSubTable: React.FC<{ items?: QC[]; fps?: number }> = ({ items = [], fps = 25 }) => {
if (!items.length) return <Text style={styles.cellText}>—</Text>;
return (
<View style={styles.subTable}>
{items.map((qc, i) => (
<View key={i} style={styles.subRow} wrap={false}>
<Text style={[styles.subCell, { width: QC_WIDTHS.tc }]} wrap={false}>
{formatTimecodeRange(qc.tc, qc.duration, fps)}
</Text>
<Text style={[styles.subCell, { width: QC_WIDTHS.category }]} wrap={false}>
{qc.category || '—'}
</Text>
<Text style={[styles.subCell, { width: QC_WIDTHS.desc }]}>{qc.desc || '—'}</Text>
</View>
))}
</View>
);
};
const QCReport: React.FC = () => {
const { log, projectName } = useDaytalog();
const rows = (log?.clips ?? []).map((clip) => ({
clip: (clip.clip || '').slice(0, 10),
scene: clip.scene,
shot: clip.shot,
take: clip.take,
qc: { qc: clip.qc ?? [], fps: clip.fps },
}));
// Fallback-safe helpers
const dayLabel = ((): string => {
try {
return String(log?.day?.() ?? '—');
} catch {
return '—';
}
})();
const dateLabel = ((): string => {
try {
return String(log?.date?.('yyyy-mm-dd') ?? log?.date?.('mmddyy') ?? '—');
} catch {
return '—';
}
})();
return (
<Document>
<Page size='A4' style={styles.page}>
<Text
style={styles.pageNumber}
render={({ pageNumber, totalPages }) =>
totalPages > 1 ? `Page ${pageNumber} / ${totalPages}` : ''
}
fixed
/>
{/* ===== Header ===== */}
<View style={styles.headerWrap}>
<View style={styles.topRow}>
<View style={styles.titleBlock}>
<Text style={styles.projectName} wrap={false}>
{projectName || 'Untitled Project'}
</Text>
<Text style={styles.title} wrap={false}>
QC Report
</Text>
</View>
<View style={styles.metaBlock}>
<Text style={styles.metaLine} wrap={false}>
Day: {dayLabel}
</Text>
<Text style={styles.metaLine} wrap={false}>
Shooting Date: {dateLabel}
</Text>
</View>
</View>
<View style={styles.divider} />
</View>
{/* ===== Table ===== */}
<View style={styles.table}>
{/* Header Row */}
<View style={styles.row} fixed>
<View style={[styles.headerCellBox, { width: COL_WIDTHS.clip }]}>
<Text style={styles.headerTitle} wrap={false}>
Clip
</Text>
</View>
<View style={[styles.headerCellBox, { width: COL_WIDTHS.scene }]}>
<Text style={styles.headerTitle} wrap={false}>
Scene
</Text>
</View>
<View style={[styles.headerCellBox, { width: COL_WIDTHS.shot }]}>
<Text style={styles.headerTitle} wrap={false}>
Shot
</Text>
</View>
<View style={[styles.headerCellBox, { width: COL_WIDTHS.take }]}>
<Text style={styles.headerTitle} wrap={false}>
Take
</Text>
</View>
{/* QC header with inline subheaders */}
<View style={[styles.headerCellBox, { width: COL_WIDTHS.qc }]}>
<Text style={styles.headerTitle} wrap={false}>
QC Notes
</Text>
<View style={styles.subHeaderRow} wrap={false}>
<Text style={[styles.subHeaderText, { width: QC_WIDTHS.tc }]} wrap={false}>
Timecode
</Text>
<Text style={[styles.subHeaderText, { width: QC_WIDTHS.category }]} wrap={false}>
Category
</Text>
<Text style={[styles.subHeaderText, { width: QC_WIDTHS.desc }]} wrap={false}>
Description
</Text>
</View>
</View>
</View>
{/* Body */}
{rows.map((row, rowIndex) => (
<View
key={rowIndex}
style={[styles.row, rowIndex % 2 === 1 ? styles.zebra : {}]}
wrap={false}
>
<View style={[styles.cellView, { width: COL_WIDTHS.clip }]}>
<Text style={styles.cellText} wrap={false}>
{row.clip ?? '—'}
</Text>
</View>
<View style={[styles.cellView, { width: COL_WIDTHS.scene }]}>
<Text style={styles.cellText} wrap={false}>
{row.scene ?? '—'}
</Text>
</View>
<View style={[styles.cellView, { width: COL_WIDTHS.shot }]}>
<Text style={styles.cellText} wrap={false}>
{row.shot ?? '—'}
</Text>
</View>
<View style={[styles.cellView, { width: COL_WIDTHS.take }]}>
<Text style={styles.cellText} wrap={false}>
{row.take ?? '—'}
</Text>
</View>
<View style={[styles.cellView, { width: COL_WIDTHS.qc }]}>
<QCSubTable items={row.qc.qc} fps={row.qc.fps} />
</View>
</View>
))}
</View>
</Page>
</Document>
);
};
/** Utilities */
function pad(n: number, l = 2) {
return String(n).padStart(l, '0');
}
function parseTcToFrames(tc?: string, fps = 25): number | null {
if (!tc) return null;
const m = tc.match(/^(\d{1,2}):(\d{2}):(\d{2})[.:](\d{2})$/);
if (!m) return null;
const [, H, M, S, F] = m;
const h = Number(H),
mnt = Number(M),
s = Number(S),
f = Number(F);
return (h * 3600 + mnt * 60 + s) * fps + f;
}
function parseDurationToFrames(dur?: string, fps = 25): number | null {
if (!dur) return null;
if (/^\d+$/.test(dur)) return Number(dur); // raw frames
const frames = parseTcToFrames(dur, fps); // TC-form duration
return frames !== null ? frames : null;
}
/** "HH:MM:SS.FF - HH:MM:SS.FF" */
function formatTimecodeRange(start?: string, duration?: string, fps = 25): string {
const startFrames = parseTcToFrames(start, fps);
const durFrames = parseDurationToFrames(duration, fps);
if (startFrames === null || durFrames === null) return start || '—';
const endFrames = startFrames + durFrames;
const toTc = (frames: number) => {
const H = Math.floor(frames / (3600 * fps));
let r = frames % (3600 * fps);
const M = Math.floor(r / (60 * fps));
r %= 60 * fps;
const S = Math.floor(r / fps);
const F = r % fps;
return `${pad(H)}:${pad(M)}:${pad(S)}.${pad(F)}`;
};
return `${toTc(startFrames)} - ${toTc(endFrames)}`;
}
export default QCReport;
Copy
day: 1
date: 2025-08-15
ocf:
clips:
- clip: A001C001
size: 7572467904
copies:
- volume: Raid
hash: 4607f64fcae33728
- volume: Master_01
hash: 4607f64fcae33728
- volume: Backup_01
hash: 4607f64fcae33728
tc_start: 10:13:25:08
tc_end: 10:14:01:13
duration: 00:00:36:05
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C002
size: 8642912484
copies:
- volume: Raid
hash: b90261cadf8aab43
- volume: Master_01
hash: b90261cadf8aab43
- volume: Backup_01
hash: b90261cadf8aab43
tc_start: 10:15:18:07
tc_end: 10:15:59:04
duration: 00:00:40:22
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C003
size: 13965590828
copies:
- volume: Raid
hash: d21f12407d0ca819
- volume: Master_01
hash: d21f12407d0ca819
- volume: Backup_01
hash: d21f12407d0ca819
tc_start: 10:16:28:14
tc_end: 10:17:35:14
duration: 00:01:07:00
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C004
size: 5912345678
copies:
- volume: Raid
hash: a1b2c3d4e5f60718
- volume: Master_01
hash: a1b2c3d4e5f60718
- volume: Backup_01
hash: a1b2c3d4e5f60718
tc_start: 10:18:02:02
tc_end: 10:18:30:20
duration: 00:00:28:18
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C005
size: 4023456789
copies:
- volume: Raid
hash: 9f8e7d6c5b4a3210
- volume: Master_01
hash: 9f8e7d6c5b4a3210
- volume: Backup_01
hash: 9f8e7d6c5b4a3210
tc_start: 10:18:45:00
tc_end: 10:19:03:10
duration: 00:00:18:10
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C006
size: 15234567890
copies:
- volume: Raid
hash: 1234abcd5678ef90
- volume: Master_01
hash: 1234abcd5678ef90
- volume: Backup_01
hash: 1234abcd5678ef90
tc_start: 10:19:10:12
tc_end: 10:20:23:00
duration: 00:01:12:13
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C007
size: 5123456780
copies:
- volume: Raid
hash: fedcba9876543210
- volume: Master_01
hash: fedcba9876543210
- volume: Backup_01
hash: fedcba9876543210
tc_start: 10:21:05:00
tc_end: 10:21:27:05
duration: 00:00:22:05
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C008
size: 9434567890
copies:
- volume: Raid
hash: 0a1b2c3d4e5f6789
- volume: Master_01
hash: 0a1b2c3d4e5f6789
- volume: Backup_01
hash: 0a1b2c3d4e5f6789
tc_start: 10:21:40:03
tc_end: 10:22:21:20
duration: 00:00:41:17
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C009
size: 7345678901
copies:
- volume: Raid
hash: baddc0ffee12beef
- volume: Master_01
hash: baddc0ffee12beef
- volume: Backup_01
hash: baddc0ffee12beef
tc_start: 10:22:35:22
tc_end: 10:23:11:00
duration: 00:00:35:03
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C010
size: 16723456789
copies:
- volume: Raid
hash: cafe0badf00d1234
- volume: Master_01
hash: cafe0badf00d1234
- volume: Backup_01
hash: cafe0badf00d1234
tc_start: 10:23:30:00
tc_end: 10:24:45:12
duration: 00:01:15:12
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C011
size: 4987654321
copies:
- volume: Raid
hash: deadbeefc001d00d
- volume: Master_01
hash: deadbeefc001d00d
- volume: Backup_01
hash: deadbeefc001d00d
tc_start: 10:25:10:10
tc_end: 10:25:38:05
duration: 00:00:27:20
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
- clip: A001C012
size: 10987654321
copies:
- volume: Raid
hash: 0ff1ce0f00dbabe1
- volume: Master_01
hash: 0ff1ce0f00dbabe1
- volume: Backup_01
hash: 0ff1ce0f00dbabe1
tc_start: 10:26:05:00
tc_end: 10:26:59:10
duration: 00:00:54:10
camera_model: CinemaCamera
reel: A001
fps: 25
sensor_fps: 25
lens: 40 mm
shutter: 180
resolution: 4608x3164
codec: ProRes 4444
gamma: LOG-C
ei: 800
wb: 5300
tint: "+0.0"
lut: commercial.cube
custom:
- schema: Silverstack
clips:
- clip: A001C001
scene: "205"
shot: "55"
take: "1"
qc:
- tc: 09:21:02.02
category: Content
duration: "16"
desc: Crew in frame
- clip: A001C002
scene: "205"
shot: "55"
take: "2"
qc:
- tc: 09:26:05.09
category: Sharpness
duration: "12"
desc: Not in focus
- clip: A001C003
scene: "205"
shot: "55"
take: "3"
qc:
- tc: 09:56:18.07
category: Sharpness
duration: "37"
desc: Not in focus
- clip: A001C004
scene: "205"
shot: "55"
take: "4"
- clip: A001C005
scene: "205"
shot: "55"
take: "5"
- clip: A001C006
scene: "205"
shot: "55"
take: "6"
qc:
- tc: 10:02:18.07
category: Sharpness
duration: "30"
desc: Not in focus
- tc: 10:03:20.07
category: Sharpness
duration: "37"
desc: Focus 1 m behind actor during dolly move.
- clip: A001C007
scene: "205"
shot: "55"
take: "7"
- clip: A001C008
scene: "205"
shot: "55"
take: "8"
- clip: A001C009
scene: "205"
shot: "55"
take: "9"
- clip: A001C010
scene: "205"
shot: "56"
take: "1"
- clip: A001C011
scene: "205"
shot: "56"
take: "2"
- clip: A001C012
scene: "205"
shot: "56"
take: "3"
version: 1
Copy
project_name: Demo
custom_schemas:
- id: Silverstack
order: 1
active: true
sync: clip
csv_import: true
clip_fields:
- type: clip
column: Name
- type: text
key_name: scene
column: Scene
- type: text
key_name: shot
column: Shot
- type: text
key_name: take
column: Take
- type: kv_map_list
key_name: qc
column: Cue Points
subfields:
- key_name: tc
- key_name: category
- key_name: duration
- key_name: desc
primary_delimiter: ;
secondary_delimiter: "|"
version: 1