From dc85cd3ff0ceb2626bd4dfae6a12e236f3dd0e6a Mon Sep 17 00:00:00 2001 From: eddy Date: Mon, 20 Jul 2026 18:58:24 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20share=20knop=20op=20dashboard=20kaart?= =?UTF-8?q?=20=E2=80=94=20deelt=20kaart=20als=20PNG=20via=20expo-sharing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 73 +++++++++++++++++++++ package.json | 2 + src/components/LatestMeasurementCard.tsx | 81 ++++++++++++++++-------- 3 files changed, 129 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4a933a..4264be1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@react-navigation/native-stack": "^7.17.6", "expo": "~54.0.0", "expo-secure-store": "~15.0.8", + "expo-sharing": "~14.0.8", "expo-sqlite": "~16.0.10", "expo-status-bar": "~3.0.9", "expo-updates": "~29.0.18", @@ -23,6 +24,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", + "react-native-view-shot": "4.0.3", "react-native-web": "^0.21.0" }, "devDependencies": { @@ -3728,6 +3730,15 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4403,6 +4414,15 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -5212,6 +5232,15 @@ "node": ">=20.16.0" } }, + "node_modules/expo-sharing": { + "version": "14.0.8", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.8.tgz", + "integrity": "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-sqlite": { "version": "16.0.10", "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-16.0.10.tgz", @@ -6144,6 +6173,19 @@ "license": "MIT", "peer": true }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -9611,6 +9653,19 @@ "react-native": "*" } }, + "node_modules/react-native-view-shot": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz", + "integrity": "sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==", + "license": "MIT", + "dependencies": { + "html2canvas": "^1.4.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", @@ -10753,6 +10808,15 @@ "node": "*" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -11087,6 +11151,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", diff --git a/package.json b/package.json index a09395c..785600e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@react-navigation/native-stack": "^7.17.6", "expo": "~54.0.0", "expo-secure-store": "~15.0.8", + "expo-sharing": "~14.0.8", "expo-sqlite": "~16.0.10", "expo-status-bar": "~3.0.9", "expo-updates": "~29.0.18", @@ -18,6 +19,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", + "react-native-view-shot": "4.0.3", "react-native-web": "^0.21.0" }, "devDependencies": { diff --git a/src/components/LatestMeasurementCard.tsx b/src/components/LatestMeasurementCard.tsx index 04976d4..1926c58 100644 --- a/src/components/LatestMeasurementCard.tsx +++ b/src/components/LatestMeasurementCard.tsx @@ -1,5 +1,7 @@ -import React from 'react' -import { View, Text, StyleSheet } from 'react-native' +import React, { useRef } from 'react' +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native' +import ViewShot from 'react-native-view-shot' +import * as Sharing from 'expo-sharing' import { Measurement } from '../types' type Props = { measurements: Measurement[] } @@ -16,6 +18,8 @@ const rows: { label: string; key: keyof Measurement; suffix: string }[] = [ ] export default function LatestMeasurementCard({ measurements }: Props) { + const viewShotRef = useRef(null) + if (measurements.length === 0) { return ( @@ -28,47 +32,70 @@ export default function LatestMeasurementCard({ measurements }: Props) { const previous = measurements[1] const diff = previous ? (latest.weight - previous.weight) : null + const handleShare = async () => { + try { + const uri = await viewShotRef.current?.capture?.() + if (uri) { + await Sharing.shareAsync(uri, { mimeType: 'image/png' }) + } + } catch {} + } + return ( - {previous && ( - - Voorlaatste meting - - {new Date(previous.measuredAt).toLocaleDateString('nl-NL')} — {previous.weight.toFixed(1)} kg - - - Verschil {diff !== null && (diff > 0 ? '+' : '')}{diff?.toFixed(1)} kg - - - - )} + + + + ↗ + + - Laatste meting - {new Date(latest.measuredAt).toLocaleDateString('nl-NL')} - {rows.map(r => ( - - {r.label} - - {typeof latest[r.key] === 'number' - ? (latest[r.key] as number).toFixed(1) - : latest[r.key]}{' '} - {r.suffix} - + + + {previous && ( + + Voorlaatste meting + + {new Date(previous.measuredAt).toLocaleDateString('nl-NL')} — {previous.weight.toFixed(1)} kg + + + Verschil {diff !== null && (diff > 0 ? '+' : '')}{diff?.toFixed(1)} kg + + + + )} + + Laatste meting + {new Date(latest.measuredAt).toLocaleDateString('nl-NL')} + {rows.map(r => ( + + {r.label} + + {typeof latest[r.key] === 'number' + ? (latest[r.key] as number).toFixed(1) + : latest[r.key]}{' '} + {r.suffix} + + + ))} - ))} + ) } const styles = StyleSheet.create({ card: { backgroundColor: '#fff', borderRadius: 12, padding: 16, margin: 16, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 8, elevation: 3 }, + header: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 }, + shareIcon: { fontSize: 22, color: '#2196F3' }, + shotContent: { backgroundColor: '#fff', padding: 16 }, title: { fontSize: 18, fontWeight: '700', marginBottom: 4 }, date: { fontSize: 14, color: '#666', marginBottom: 12 }, row: { flexDirection: 'row', paddingVertical: 4 }, rowLabel: { fontSize: 14, color: '#555', flex: 1 }, rowValue: { fontSize: 14, fontWeight: '600' }, emptyText: { fontSize: 16, color: '#999', textAlign: 'center', padding: 20 }, - previous: { marginTop: 4 }, + previous: { marginBottom: 4 }, divider: { height: 1, backgroundColor: '#eee', marginVertical: 12 }, previousTitle: { fontSize: 18, fontWeight: '700', marginBottom: 4 }, previousDate: { fontSize: 14, color: '#666', marginBottom: 4 },