feat: share knop op dashboard kaart — deelt kaart als PNG via expo-sharing

This commit is contained in:
eddy 2026-07-20 18:58:24 +02:00
commit dc85cd3ff0
3 changed files with 129 additions and 27 deletions

73
package-lock.json generated
View file

@ -13,6 +13,7 @@
"@react-navigation/native-stack": "^7.17.6", "@react-navigation/native-stack": "^7.17.6",
"expo": "~54.0.0", "expo": "~54.0.0",
"expo-secure-store": "~15.0.8", "expo-secure-store": "~15.0.8",
"expo-sharing": "~14.0.8",
"expo-sqlite": "~16.0.10", "expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"expo-updates": "~29.0.18", "expo-updates": "~29.0.18",
@ -23,6 +24,7 @@
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1", "react-native-svg": "15.12.1",
"react-native-view-shot": "4.0.3",
"react-native-web": "^0.21.0" "react-native-web": "^0.21.0"
}, },
"devDependencies": { "devDependencies": {
@ -3728,6 +3730,15 @@
"node": "18 || 20 || >=22" "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": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -4403,6 +4414,15 @@
"hyphenate-style-name": "^1.0.3" "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": { "node_modules/css-select": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@ -5212,6 +5232,15 @@
"node": ">=20.16.0" "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": { "node_modules/expo-sqlite": {
"version": "16.0.10", "version": "16.0.10",
"resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-16.0.10.tgz", "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-16.0.10.tgz",
@ -6144,6 +6173,19 @@
"license": "MIT", "license": "MIT",
"peer": true "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": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@ -9611,6 +9653,19 @@
"react-native": "*" "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": { "node_modules/react-native-web": {
"version": "0.21.2", "version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@ -10753,6 +10808,15 @@
"node": "*" "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": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -11087,6 +11151,15 @@
"node": ">= 0.4.0" "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": { "node_modules/uuid": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",

View file

@ -8,6 +8,7 @@
"@react-navigation/native-stack": "^7.17.6", "@react-navigation/native-stack": "^7.17.6",
"expo": "~54.0.0", "expo": "~54.0.0",
"expo-secure-store": "~15.0.8", "expo-secure-store": "~15.0.8",
"expo-sharing": "~14.0.8",
"expo-sqlite": "~16.0.10", "expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"expo-updates": "~29.0.18", "expo-updates": "~29.0.18",
@ -18,6 +19,7 @@
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1", "react-native-svg": "15.12.1",
"react-native-view-shot": "4.0.3",
"react-native-web": "^0.21.0" "react-native-web": "^0.21.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,5 +1,7 @@
import React from 'react' import React, { useRef } from 'react'
import { View, Text, StyleSheet } from 'react-native' 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' import { Measurement } from '../types'
type Props = { measurements: Measurement[] } type Props = { measurements: Measurement[] }
@ -16,6 +18,8 @@ const rows: { label: string; key: keyof Measurement; suffix: string }[] = [
] ]
export default function LatestMeasurementCard({ measurements }: Props) { export default function LatestMeasurementCard({ measurements }: Props) {
const viewShotRef = useRef<ViewShot>(null)
if (measurements.length === 0) { if (measurements.length === 0) {
return ( return (
<View style={styles.card}> <View style={styles.card}>
@ -28,8 +32,26 @@ export default function LatestMeasurementCard({ measurements }: Props) {
const previous = measurements[1] const previous = measurements[1]
const diff = previous ? (latest.weight - previous.weight) : null 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 ( return (
<View style={styles.card}> <View style={styles.card}>
<View style={styles.header}>
<View style={{ flex: 1 }} />
<TouchableOpacity onPress={handleShare} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Text style={styles.shareIcon}>↗</Text>
</TouchableOpacity>
</View>
<ViewShot ref={viewShotRef} options={{ format: 'png', quality: 1 }}>
<View style={styles.shotContent}>
{previous && ( {previous && (
<View style={styles.previous}> <View style={styles.previous}>
<Text style={styles.previousTitle}>Voorlaatste meting</Text> <Text style={styles.previousTitle}>Voorlaatste meting</Text>
@ -57,18 +79,23 @@ export default function LatestMeasurementCard({ measurements }: Props) {
</View> </View>
))} ))}
</View> </View>
</ViewShot>
</View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { backgroundColor: '#fff', borderRadius: 12, padding: 16, margin: 16, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 8, elevation: 3 }, 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 }, title: { fontSize: 18, fontWeight: '700', marginBottom: 4 },
date: { fontSize: 14, color: '#666', marginBottom: 12 }, date: { fontSize: 14, color: '#666', marginBottom: 12 },
row: { flexDirection: 'row', paddingVertical: 4 }, row: { flexDirection: 'row', paddingVertical: 4 },
rowLabel: { fontSize: 14, color: '#555', flex: 1 }, rowLabel: { fontSize: 14, color: '#555', flex: 1 },
rowValue: { fontSize: 14, fontWeight: '600' }, rowValue: { fontSize: 14, fontWeight: '600' },
emptyText: { fontSize: 16, color: '#999', textAlign: 'center', padding: 20 }, emptyText: { fontSize: 16, color: '#999', textAlign: 'center', padding: 20 },
previous: { marginTop: 4 }, previous: { marginBottom: 4 },
divider: { height: 1, backgroundColor: '#eee', marginVertical: 12 }, divider: { height: 1, backgroundColor: '#eee', marginVertical: 12 },
previousTitle: { fontSize: 18, fontWeight: '700', marginBottom: 4 }, previousTitle: { fontSize: 18, fontWeight: '700', marginBottom: 4 },
previousDate: { fontSize: 14, color: '#666', marginBottom: 4 }, previousDate: { fontSize: 14, color: '#666', marginBottom: 4 },