mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 16:46:41 +02:00
✨ 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:
parent
3caf731afd
commit
be8e41fece
5 changed files with 199 additions and 8 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
94
apps/figgos/apps/mobile/utils/share-figure.ts
Normal file
94
apps/figgos/apps/mobile/utils/share-figure.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue