diff --git a/App.js b/App.js index 5d78051..4807119 100644 --- a/App.js +++ b/App.js @@ -37,6 +37,7 @@ import Providers from "./src/providers"; import { LogBox } from "react-native"; import CommentsBottomSheet from "./src/components/bottomsheets/CommentsBottomSheet"; import "./src/utils/Sheet"; +import ShareQrModalContainer from "./src/components/modal/ShareQrModalContainer"; console.disableYellowBox = true; console.reportErrorsAsExceptions = false; @@ -173,6 +174,7 @@ const App = () => { + {/* */} diff --git a/src/components/AppActionSheet.js b/src/components/AppActionSheet.js index e2de421..996c752 100644 --- a/src/components/AppActionSheet.js +++ b/src/components/AppActionSheet.js @@ -3,6 +3,7 @@ import React from "react"; import { Platform } from "react-native"; import ActionSheet from "react-native-actions-sheet"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { isWeb } from "../hooks/useLayoutType"; import { gutters, Palette } from "../styles"; const AppActionSheet = ({ children, props }) => { @@ -19,7 +20,7 @@ const AppActionSheet = ({ children, props }) => { {...props} > { + const { matrix } = useMemo(() => { + return generateQrMatrix({ value, errorCorrectionLevel }); + }, [value, errorCorrectionLevel]); + + const cellSize = useMemo(() => { + if (!matrix || !matrix.length) { + return 0; + } + return size / matrix.length; + }, [matrix, size]); + + return ( + + + {matrix.map((row, rowIndex) => { + return row.map((isDark, colIndex) => { + if (!isDark) { + return null; + } + + const key = `${rowIndex}-${colIndex}`; + + return ( + + ); + }); + })} + + + ); +}; + +export default QrCode; + diff --git a/src/components/modal/ShareModal.js b/src/components/modal/ShareModal.js index e85befa..3f00bbd 100644 --- a/src/components/modal/ShareModal.js +++ b/src/components/modal/ShareModal.js @@ -1,3 +1,8 @@ +import { + Feather, + FontAwesome, + MaterialCommunityIcons, +} from "@expo/vector-icons"; import React, { useCallback, useEffect, @@ -15,21 +20,18 @@ import { View, } from "react-native"; import { SheetManager } from "react-native-actions-sheet"; -import { - Feather, - FontAwesome, - MaterialCommunityIcons, -} from "@expo/vector-icons"; -import AppActionSheet from "../AppActionSheet"; -import { Palette } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; -import { isWeb } from "../../hooks/useLayoutType"; +import { BlurView } from "expo-blur"; +import { setGlobal } from "reactn"; import { musiclandShareHeading, musiclandShareMessage, musiclandShareUrl, } from "../../data"; +import { isWeb } from "../../hooks/useLayoutType"; +import { Palette } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; +import AppActionSheet from "../AppActionSheet"; const ShareModal = ({ payload }) => { const heading = payload?.heading ?? musiclandShareHeading; @@ -38,9 +40,12 @@ const ShareModal = ({ payload }) => { const shareMessage = payload?.shareMessage ?? musiclandShareMessage; const linkLabel = payload?.linkLabel ?? url; const sectionTitle = payload?.sectionTitle ?? "Partager sur..."; + const qrTitle = payload?.qrTitle ?? "Scannez le QR-code"; + const qrUrl = payload?.qrUrl ?? musiclandShareUrl; const [copied, setCopied] = useState(false); const copyTimeout = useRef(null); + const qrOpenTimeout = useRef(null); const closeSheet = useCallback(() => { SheetManager.hide("Share"); @@ -61,6 +66,9 @@ const ShareModal = ({ payload }) => { if (copyTimeout.current) { clearTimeout(copyTimeout.current); } + if (qrOpenTimeout.current) { + clearTimeout(qrOpenTimeout.current); + } }; }, []); @@ -123,6 +131,22 @@ const ShareModal = ({ payload }) => { const handleShareOption = useCallback( async (option) => { + if (option?.type === "qr") { + const qrTitleValue = qrTitle; + + closeSheet(); + qrOpenTimeout.current = setTimeout(() => { + setGlobal({ + shareQrModal: { + visible: true, + url: qrUrl, + title: qrTitleValue, + }, + }); + }, 200); + return; + } + const target = option?.target; if (!target) { @@ -151,7 +175,7 @@ const ShareModal = ({ payload }) => { await handleSystemShare(); closeSheet(); }, - [closeSheet, handleSystemShare] + [closeSheet, handleSystemShare, qrTitle, qrUrl] ); const onSystemShare = useCallback(async () => { @@ -193,11 +217,11 @@ const ShareModal = ({ payload }) => { }, { key: "qr", + type: "qr", label: "Partager par QR-code", Icon: (props) => ( ), - target: `https://api.qrserver.com/v1/create-qr-code/?size=512x512&data=${encodedUrl}`, }, ]; }, [encodedShareText, encodedUrl, payload?.options, shareTitle]); @@ -205,7 +229,12 @@ const ShareModal = ({ payload }) => { return ( - + {heading} @@ -232,31 +261,39 @@ const ShareModal = ({ payload }) => { - {copied ? ( - Lien copie - ) : null} - + {copied ? Lien copié : null} + - + {sectionTitle} - {shareOptions.map((option) => { + {shareOptions.map((option, index) => { const Icon = option?.Icon ?? Feather; return ( - handleShareOption(option)} - style={styles.optionRow} + - - - - {option?.label} - + handleShareOption(option)} + style={styles.optionRow} + > + + + + {option?.label} + + ); })} - + ); @@ -267,7 +304,8 @@ const styles = StyleSheet.create({ gap: 24, }, card: { - backgroundColor: Palette.glass, + // backgroundColor: Palette.glass, + overflow: "hidden", borderRadius: 24, borderWidth: 1, borderColor: "rgba(255, 255, 255, 0.08)", @@ -275,6 +313,9 @@ const styles = StyleSheet.create({ paddingVertical: 18, gap: 16, }, + blurCard: { + overflow: "hidden", + }, heading: { fontSize: 20, color: Palette.white, @@ -325,11 +366,15 @@ const styles = StyleSheet.create({ optionsList: { gap: 12, }, + optionBlur: { + borderRadius: 18, + overflow: "hidden", + }, optionRow: { flexDirection: "row", alignItems: "center", backgroundColor: "rgba(255, 255, 255, 0.04)", - borderRadius: 18, + // borderRadius: 18, paddingHorizontal: 14, paddingVertical: 14, gap: 14, @@ -351,4 +396,3 @@ const styles = StyleSheet.create({ }); export default ShareModal; - diff --git a/src/components/modal/ShareQrModal.js b/src/components/modal/ShareQrModal.js new file mode 100644 index 0000000..c73a5f8 --- /dev/null +++ b/src/components/modal/ShareQrModal.js @@ -0,0 +1,116 @@ +import React, { useMemo } from "react"; +import { BlurView } from "expo-blur"; +import { Platform, Pressable, StyleSheet, Text, View } from "react-native"; +import { Feather } from "@expo/vector-icons"; +import { Portal } from "@gorhom/portal"; + +import QrCode from "../QrCode"; +import { Palette } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; + +const ShareQrModal = ({ visible, onClose, url, title = "Scannez le QR-code" }) => { + const shareUrl = useMemo(() => url, [url]); + + if (!visible) { + return null; + } + + const blurIntensity = Platform.select({ ios: 40, default: 20 }); + + return ( + + + + + + + + + + + + {title} + + Partage ce code pour guider tes contacts directement vers MusicLand. + + + + + + + + {shareUrl} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + backdrop: { + backgroundColor: "rgba(0, 0, 0, 0.65)", + }, + centerWrapper: { + flex: 1, + alignItems: "center", + justifyContent: "center", + padding: 24, + }, + modalCard: { + width: 360, + maxWidth: "90%", + borderRadius: 28, + overflow: "hidden", + paddingHorizontal: 24, + paddingVertical: 32, + backgroundColor: Palette.glass, + }, + closeButton: { + position: "absolute", + top: 18, + right: 18, + width: 36, + height: 36, + borderRadius: 18, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255, 255, 255, 0.12)", + }, + content: { + alignItems: "center", + gap: 18, + }, + title: { + fontSize: 20, + color: Palette.white, + fontFamily: FONT_FAMILY.HelveticaNeueMedium, + }, + subtitle: { + fontSize: 14, + color: Palette.white, + opacity: 0.8, + textAlign: "center", + fontFamily: FONT_FAMILY.InterRegular, + paddingHorizontal: 8, + }, + qrWrapper: { + padding: 16, + borderRadius: 24, + backgroundColor: "rgba(255, 255, 255, 0.08)", + }, + linkText: { + fontSize: 13, + color: Palette.white, + opacity: 0.9, + textAlign: "center", + fontFamily: FONT_FAMILY.InterMedium, + }, +}); + +export default ShareQrModal; diff --git a/src/components/modal/ShareQrModalContainer.js b/src/components/modal/ShareQrModalContainer.js new file mode 100644 index 0000000..839515b --- /dev/null +++ b/src/components/modal/ShareQrModalContainer.js @@ -0,0 +1,30 @@ +import React from "react"; +import { useGlobal } from "reactn"; + +import { musiclandShareUrl } from "../../data"; +import ShareQrModal from "./ShareQrModal"; + +const ShareQrModalContainer = () => { + const [shareQrModal, setShareQrModal] = useGlobal("shareQrModal"); + + const { visible, url, title } = shareQrModal || {}; + + const handleClose = () => { + setShareQrModal({ + visible: false, + url: null, + title: "", + }); + }; + + return ( + + ); +}; + +export default ShareQrModalContainer; diff --git a/src/config/initialGlobalState.js b/src/config/initialGlobalState.js index 62a8275..1376e26 100644 --- a/src/config/initialGlobalState.js +++ b/src/config/initialGlobalState.js @@ -8,6 +8,12 @@ export default { webBackgroundImg: null, webLayoutMode: "default", + shareQrModal: { + visible: false, + url: null, + title: "", + }, + _config: { colors: { primary: Palette.primary, diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js index 88b7b63..691cb23 100644 --- a/src/screens/HitParade/HitParade.js +++ b/src/screens/HitParade/HitParade.js @@ -187,7 +187,7 @@ const HitParade = () => { backgroundImg={isWeb ? background.libraryBgWeb : background.hitParadeBG} headerType="NONE" width="100%" - maxWidth={null} + maxWidth={800} blurIntensity={isWeb ? 2 : 0} > diff --git a/src/screens/Library/components/MyMusic.js b/src/screens/Library/components/MyMusic.js index 91cf9b4..ddc1867 100644 --- a/src/screens/Library/components/MyMusic.js +++ b/src/screens/Library/components/MyMusic.js @@ -1,18 +1,23 @@ import React, { useState } from "react"; import { View } from "react-native"; +import { SheetManager } from "react-native-actions-sheet"; +import { useGlobal } from "reactn"; +import MoreMenu from "../../../components/MoreMenu"; +import { projectsRef } from "../../../config/firebase"; import { Routes } from "../../../navigation"; import { navigate } from "../../../navigation/NavigationService"; import { useUser } from "../../../providers/UserDataProvider"; import CardContainer from "./CardContainer"; import MusicCard from "./MusicCard"; -import MoreMenu from "../../../components/MoreMenu"; - const MyMusic = () => { const { userProjects = [] } = useUser(); const [menuPosition, setMenuPosition] = useState(null); const [showMenu, setShowMenu] = useState(false); const [selectedProjectId, setSelectedProjectId] = useState(null); const projects = Array.isArray(userProjects) ? userProjects.slice(0, 3) : []; + + const [, setTooltip] = useGlobal("_tooltip"); + return ( { setSelectedProjectId(p.id); setMenuPosition(posTop); setShowMenu( - (prev) => - !prev || posTop?.top !== (menuPosition?.top ?? null), + (prev) => !prev || posTop?.top !== (menuPosition?.top ?? null) ); }} /> @@ -52,6 +56,34 @@ const MyMusic = () => { onClose={() => setShowMenu(false)} inPlaylist={false} projectId={selectedProjectId} + extraItems={[ + { + label: "Supprimer", + onPress: () => + SheetManager.show("Delete", { + payload: { + title: "Supprimer le projet", + message: + "Cette action supprimera définitivement ce projet.", + onConfirm: async () => { + try { + if (!selectedProjectId) return; + await projectsRef.doc(selectedProjectId).delete(); + setTooltip({ + type: "success", + text: "Projet supprimé", + }); + } catch (e) { + setTooltip({ + type: "error", + text: e?.message || "Suppression impossible", + }); + } + }, + }, + }), + }, + ]} /> diff --git a/src/screens/Profile/components/EditInput.js b/src/screens/Profile/components/EditInput.js index b0cd1a3..c9d5dfc 100644 --- a/src/screens/Profile/components/EditInput.js +++ b/src/screens/Profile/components/EditInput.js @@ -1,16 +1,10 @@ -import { - View, - Text, - TextInput, - Image, - Pressable, - Platform, -} from "react-native"; +import { BlurView } from "expo-blur"; import React, { useState } from "react"; +import { Platform, Pressable, Text, TextInput, View } from "react-native"; +import EyeSlashSVG from "../../../assets/UI/EyeSlashSVG"; +import EyeSVG from "../../../assets/UI/EyeSVG"; import { Palette, Style } from "../../../styles"; import { FONT_FAMILY } from "../../../styles/Fonts"; -import { BlurView } from "expo-blur"; -import { icons } from "../../../assets"; const EditInput = ({ label = "", @@ -79,7 +73,7 @@ const EditInput = ({ /> {type === "password" && ( setShowPassword(!showPassword)}> - + {showPassword ? : } )} diff --git a/src/utils/generateQrMatrix.js b/src/utils/generateQrMatrix.js new file mode 100644 index 0000000..92e4ec1 --- /dev/null +++ b/src/utils/generateQrMatrix.js @@ -0,0 +1,37 @@ +import QRCode from "qrcode-terminal/vendor/QRCode"; +import QRErrorCorrectLevel from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel"; + +const getErrorLevel = (level) => { + if (typeof level === "number") { + return level; + } + + const normalized = String(level || "M").toUpperCase(); + return QRErrorCorrectLevel?.[normalized] ?? QRErrorCorrectLevel.M; +}; + +export const generateQrMatrix = ({ + value, + errorCorrectionLevel = "M", +}) => { + if (!value) { + throw new Error("generateQrMatrix requires a value"); + } + + const qr = new QRCode(-1, getErrorLevel(errorCorrectionLevel)); + qr.addData(value); + qr.make(); + + const size = qr.getModuleCount(); + const matrix = new Array(size); + + for (let row = 0; row < size; row += 1) { + matrix[row] = new Array(size); + for (let col = 0; col < size; col += 1) { + matrix[row][col] = qr.isDark(row, col); + } + } + + return { matrix, size }; +}; + diff --git a/src/utils/shareSheet.js b/src/utils/shareSheet.js index 5095f55..d8ec1b1 100644 --- a/src/utils/shareSheet.js +++ b/src/utils/shareSheet.js @@ -11,6 +11,7 @@ const defaultPayload = { url: musiclandShareUrl, shareTitle: "MusicLand", shareMessage: musiclandShareMessage, + qrUrl: musiclandShareUrl, }; export const openShareSheet = (payload = {}) => { @@ -21,4 +22,3 @@ export const openShareSheet = (payload = {}) => { }, }); }; -