feat(figgos): share card via native share sheet with styled message

Add "Share It" button (neo-brutalist style) to carousel and collection
card overlays. Downloads card image, shares with Unicode-styled message
including bold rarity, stat bars, drop rate and serial number.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chr1st1anG 2026-02-12 23:46:16 +01:00
parent 3caf731afd
commit be8e41fece
5 changed files with 199 additions and 8 deletions

View file

@ -11,9 +11,11 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFocusEffect } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import type { FigureResponse, FigureRarity } from '@figgos/shared';
import { api } from '../../services/api';
import FlippableCard from '../../components/FlippableCard';
import { shareFigure } from '../../utils/share-figure';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CARD_WIDTH = SCREEN_WIDTH * 0.8;
@ -185,6 +187,42 @@ export default function CarouselScreen() {
Drag to rotate · Double-tap to flip
</Text>
<FlippableCard figure={selected} maxWidth={SCREEN_WIDTH - 48} />
<Pressable
onPress={() => shareFigure(selected)}
className="active:opacity-90"
style={{ marginTop: 24 }}
>
<View style={{ position: 'relative' }}>
<View
className="bg-primary-dark rounded-lg"
style={{ position: 'absolute', top: 5, left: 4, right: -4, bottom: -5 }}
/>
<View
className="bg-primary rounded-lg flex-row items-center justify-center"
style={{
paddingHorizontal: 20,
paddingVertical: 12,
borderWidth: 3,
borderColor: 'rgb(255, 224, 102)',
gap: 8,
}}
>
<Ionicons name="paper-plane" size={18} color="rgb(15, 15, 30)" />
<Text
style={{
fontSize: 15,
fontWeight: '900',
letterSpacing: 2,
textTransform: 'uppercase',
color: 'rgb(15, 15, 30)',
}}
>
Share It
</Text>
</View>
</View>
</Pressable>
</View>
)}
</Pressable>

View file

@ -11,9 +11,11 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFocusEffect } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import type { FigureResponse } from '@figgos/shared';
import { api } from '../../services/api';
import FlippableCard from '../../components/FlippableCard';
import { shareFigure } from '../../utils/share-figure';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const GAP = 10;
@ -157,6 +159,41 @@ export default function CollectionScreen() {
Drag to rotate · Double-tap to flip
</Text>
<FlippableCard figure={selected} maxWidth={SCREEN_WIDTH - 48} />
<Pressable
onPress={() => shareFigure(selected)}
className="active:opacity-90"
style={{ marginTop: 24 }}
>
<View style={{ position: 'relative' }}>
<View
className="bg-primary-dark rounded-lg"
style={{ position: 'absolute', top: 5, left: 4, right: -4, bottom: -5 }}
/>
<View
className="bg-primary rounded-lg flex-row items-center justify-center"
style={{
paddingHorizontal: 20,
paddingVertical: 12,
borderWidth: 3,
borderColor: 'rgb(255, 224, 102)',
gap: 8,
}}
>
<Ionicons name="paper-plane" size={18} color="rgb(15, 15, 30)" />
<Text
style={{
fontSize: 15,
fontWeight: '900',
letterSpacing: 2,
textTransform: 'uppercase',
color: 'rgb(15, 15, 30)',
}}
>
Share It
</Text>
</View>
</View>
</Pressable>
</View>
)}
</Pressable>

View file

@ -22,12 +22,14 @@
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-file-system": "^19.0.21",
"expo-font": "~14.0.11",
"expo-image-manipulator": "^14.0.8",
"expo-image-picker": "^17.0.10",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-sharing": "~14.0.7",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.10",

View file

@ -0,0 +1,94 @@
import { Share, Platform, Alert } from 'react-native';
import * as Sharing from 'expo-sharing';
import { File, Paths } from 'expo-file-system/next';
import type { FigureResponse, FigureRarity } from '@figgos/shared';
const RARITY_EMOJI: Record<FigureRarity, string> = {
common: '⚪',
rare: '🔵',
epic: '🟣',
legendary: '🔥',
};
const RARITY_DROP_RATE: Record<FigureRarity, string> = {
common: '60%',
rare: '25%',
epic: '12%',
legendary: '3%',
};
let sharing = false;
export async function shareFigure(figure: FigureResponse): Promise<void> {
if (!figure.imageUrl || sharing) return;
sharing = true;
try {
const ext = figure.imageUrl.endsWith('.webp') ? 'webp' : 'png';
const destination = new File(Paths.cache, `figgos-share-${figure.id}.${ext}`);
if (!destination.exists) {
await File.downloadFileAsync(figure.imageUrl, destination);
}
if (Platform.OS === 'ios') {
await Share.share({
message: buildShareMessage(figure),
url: destination.uri,
});
} else {
await Sharing.shareAsync(destination.uri, {
mimeType: ext === 'webp' ? 'image/webp' : 'image/png',
dialogTitle: `Share ${figure.name}`,
});
}
} catch {
Alert.alert('Share failed', 'Could not share this figure. Please try again.');
} finally {
sharing = false;
}
}
// Unicode Mathematical Bold map for A-Z, 0-9
const BOLD_UPPER: Record<string, string> = {
A: '𝗔', B: '𝗕', C: '𝗖', D: '𝗗', E: '𝗘', F: '𝗙', G: '𝗚', H: '𝗛', I: '𝗜',
J: '𝗝', K: '𝗞', L: '𝗟', M: '𝗠', N: '𝗡', O: '𝗢', P: '𝗣', Q: '𝗤', R: '𝗥',
S: '𝗦', T: '𝗧', U: '𝗨', V: '𝗩', W: '𝗪', X: '𝗫', Y: '𝗬', Z: '𝗭',
'0': '𝟬', '1': '𝟭', '2': '𝟮', '3': '𝟯', '4': '𝟰',
'5': '𝟱', '6': '𝟲', '7': '𝟳', '8': '𝟴', '9': '𝟵',
};
function toBold(text: string): string {
return [...text].map((c) => BOLD_UPPER[c] ?? c).join('');
}
function statBar(value: number): string {
const filled = Math.round(value / 10);
return '▰'.repeat(filled) + '▱'.repeat(10 - filled);
}
function buildShareMessage(figure: FigureResponse): string {
const emoji = RARITY_EMOJI[figure.rarity];
const rate = RARITY_DROP_RATE[figure.rarity];
const stats = figure.generatedProfile?.stats;
const serial = figure.id.split('-').pop()?.toUpperCase();
const line = '━━━━━━━━━━━━━';
let msg = line;
msg += `\n${emoji} ${toBold(figure.rarity.toUpperCase() + ' PULL')}`;
msg += `\n${line}`;
msg += `\n"${figure.name}"`;
if (stats) {
msg += `\nATK ${statBar(stats.attack)} ${stats.attack}`;
msg += `\nDEF ${statBar(stats.defense)} ${stats.defense}`;
msg += `\nSPL ${statBar(stats.special)} ${stats.special}`;
}
msg += `\n${serial ? `#${serial} · ` : ''}${rate} drop rate`;
msg += `\n${line}`;
msg += `\nThink you can pull better? ✨`;
msg += `\nMade with Figgos`;
return msg;
}