Skip to content

Commit

Permalink
Fixs upload profile picture (#876)
Browse files Browse the repository at this point in the history
  • Loading branch information
OverGlass authored Oct 7, 2024
1 parent f4907a0 commit 7d66670
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 104 deletions.
16 changes: 12 additions & 4 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const ContainedFrame = styled(ButtonFrameStyled, {
name: 'VoxButtonContained',
})

const InverseContainedFrame = styled(ButtonFrameStyled, {
name: 'VoxButtonInverseContained',
})

const OutlinedFrame = styled(ButtonFrameStyled, {
name: 'VoxButtonOutlined',
borderColor: '$borderColor',
Expand All @@ -100,7 +104,7 @@ const SoftFrame = styled(ButtonFrameStyled, {
name: 'VoxButtonSoft',
})

const getFrame = (variant?: 'outlined' | 'text' | 'soft' | 'contained') => {
const getFrame = (variant?: 'outlined' | 'text' | 'soft' | 'contained', inverse?: boolean) => {
switch (variant) {
case 'outlined':
return OutlinedFrame
Expand All @@ -110,12 +114,16 @@ const getFrame = (variant?: 'outlined' | 'text' | 'soft' | 'contained') => {
return SoftFrame
case 'contained':
default:
return ContainedFrame
return inverse ? InverseContainedFrame : ContainedFrame
}
}

const ButtonFrame = ({ variant, ...props }: React.ComponentProps<typeof ButtonFrameStyled> & { variant?: 'outlined' | 'text' | 'soft' | 'contained' }) => {
const Frame = getFrame(variant)
const ButtonFrame = ({
variant,
inverse,
...props
}: React.ComponentProps<typeof ButtonFrameStyled> & { variant?: 'outlined' | 'text' | 'soft' | 'contained'; inverse?: boolean }) => {
const Frame = getFrame(variant, inverse)

return <Frame {...props} />
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/ModalOrPageBase/ModalOrPageBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ const styles = StyleSheet.create({
},
modalView: {
backgroundColor: 'white',
borderRadius: 20,
borderRadius: 32,
margin: Spacing.largeMargin,
alignItems: 'center',
cursor: 'auto',
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: {
width: 0,
Expand Down
5 changes: 4 additions & 1 deletion src/components/ProfileCards/ProfileCard/MyProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export default function MyProfileCard() {
const { session } = useSession()
const { user: credentials } = useUserStore()
const profile = user?.data
const onNavigateToCadre = useCallback(() => openURL(`${credentials?.isAdmin ? clientEnv.ADMIN_URL : clientEnv.OAUTH_BASE_URL}${profile.cadre_auth_path}`), [])
const onNavigateToCadre = useCallback(
() => openURL(`${credentials?.isAdmin ? clientEnv.ADMIN_URL : clientEnv.OAUTH_BASE_URL}${profile?.cadre_auth_path}`),
[profile],
)

if (!profile) {
return null
Expand Down
26 changes: 20 additions & 6 deletions src/components/ProfilePicture/ProfilePicture.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ComponentPropsWithRef } from 'react'
import { Platform } from 'react-native'
import { GetThemeValueForKey } from '@tamagui/web'
import { BlurView } from 'expo-blur'
import { Image } from 'expo-image'
import { Circle, CircleProps, getTokenValue, Spinner, Square, SquareProps, Token } from 'tamagui'
import { Circle, CircleProps, getTokenValue, Spinner, Square, SquareProps, Token, YStack, ZStack } from 'tamagui'
import Text from '../base/Text'

type ProfilePictureProps = {
Expand Down Expand Up @@ -32,29 +34,41 @@ const ProfilePicture = (props: ProfilePictureProps) => {
.join('')

const Shape = rounded ? Circle : Square

const sizeValue = getTokenValue(size, 'size')
const content = src ? (
<Image
alt={alt}
source={{ uri: src }}
key={src}
style={{
width: sizeValue,
height: sizeValue,
}}
/>
) : (
<Text color={textColor ?? '$blue4'} fontSize={Platform.OS === 'ios' ? sizeValue / 2 : sizeValue / 2.5} fontWeight={props.fontWeight ?? '$2'}>
<Text
color={textColor ?? '$blue4'}
fontSize={Platform.OS === 'ios' ? sizeValue / 2 : sizeValue / 2.5}
textAlign="center"
fontWeight={props.fontWeight ?? '$2'}
>
{initials}
</Text>
)

return (
<Shape backgroundColor={backgroundColor || '$blue2'} size={size} {...rest} overflow="hidden">
{props.loading ? <Spinner color={textColor ?? '$blue4'} /> : content}
<ZStack flex={1} width="100%">
<YStack x={0} height="100%" flex={1} justifyContent="center" alignContent="center">
{content}
</YStack>
{props.loading ? (
<YStack x={0} height="100%" flex={1} justifyContent="center" alignContent="center">
<Spinner color={textColor ?? '$white1'} />
</YStack>
) : null}
</ZStack>
</Shape>
)
}

export default ProfilePicture
export default (props: ComponentPropsWithRef<typeof ProfilePicture>) => <ProfilePicture {...props} key={props.src} />
24 changes: 14 additions & 10 deletions src/components/base/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { NamedExoticComponent, useCallback } from 'react'
import React, { NamedExoticComponent, useCallback, useEffect } from 'react'
import { FlatList, Modal, TouchableOpacity } from 'react-native'
import Text from '@/components/base/Text'
import { useLazyRef } from '@/hooks/useLazyRef'
Expand Down Expand Up @@ -76,8 +76,8 @@ const DropdownFrame = styled(ThemeableStack, {
backgroundColor: 'white',
borderRadius: 16,
overflow: 'hidden',
elevation: 2,
shadowColor: '$gray6',
elevation: 1,
shadowColor: '$gray1',
borderWidth: 1,
borderColor: '$textOutline',
variants: {
Expand Down Expand Up @@ -119,17 +119,21 @@ function Dropdown({ items, onSelect, value, ...props }: DropdownProps) {
)
}

export function DropdownWrapper({ children, onSelect, ...props }: DropdownProps & { children: React.ReactNode }) {
const [open, setOpen] = React.useState(false)
export function DropdownWrapper({
children,
onSelect,
...props
}: DropdownProps & { children: React.ReactNode; open?: boolean; onOpenChange?: (x: boolean) => void }) {
const open = props.open ?? false
const setOpen = props.onOpenChange ?? (() => {})
const container = React.useRef<TouchableOpacity | null>(null)
const [dropdownTop, setDropdownTop] = React.useState(0)
const handleOpen = useCallback(() => {
if (!container.current) return
useEffect(() => {
if (!container.current || !props.open) return
container.current.measure((_fx, _fy, _w, h, _px, py) => {
setDropdownTop(py + h)
})
setOpen(true)
}, [])
}, [props.open])

const handleClose = useCallback(() => {
setOpen(false)
Expand All @@ -141,7 +145,7 @@ export function DropdownWrapper({ children, onSelect, ...props }: DropdownProps
}, [])

return (
<TouchableOpacity ref={container} onPress={handleOpen}>
<TouchableOpacity ref={container}>
<Modal visible={open} transparent animationType="fade" onRequestClose={handleClose}>
<TouchableOpacity style={{ flex: 1 }} onPress={handleClose}>
<Dropdown {...props} onSelect={handleSelect} position="absolute" top={dropdownTop} alignSelf="center" minWidth={230} />
Expand Down
129 changes: 58 additions & 71 deletions src/screens/profil/dashboard/components/CropImg.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React, { ComponentProps, useEffect, useRef, useState } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { Dimensions, SafeAreaView, StyleSheet, View } from 'react-native'
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { VoxButton } from '@/components/Button'
import ModalOrPageBase from '@/components/ModalOrPageBase/ModalOrPageBase'
import VoxCard from '@/components/VoxCard/VoxCard'
import { useMutation } from '@tanstack/react-query'
import { ImageResult, manipulateAsync, SaveFormat } from 'expo-image-manipulator'
import { isWeb, styled, ThemeableStack, useMedia, XStack, YStack } from 'tamagui'

const styles = StyleSheet.create({
container: {
position: 'relative',
overflow: 'hidden',
backgroundColor: 'black',
backgroundColor: '$gray5',
},
flex: {
flex: 1,
Expand All @@ -37,36 +38,19 @@ function clamp(val: number, min: number, max: number) {
}
const CROP_SIZE = 300

const getMinImgWidthForCrop = (img: ImageResult) => {
const imageMetaData = img
const sizes = { width: imageMetaData.width, height: imageMetaData.height }
const smallerSideLabel = sizes.width > sizes.height ? 'height' : 'width'
const smallerSide = sizes[smallerSideLabel]
if (smallerSide < CROP_SIZE) {
const calcNewWidth = smallerSideLabel === 'width' ? CROP_SIZE : (CROP_SIZE * sizes.width) / sizes.height
const calcNewHeight = smallerSideLabel === 'height' ? CROP_SIZE : (CROP_SIZE * sizes.height) / sizes.width
return { width: calcNewWidth, height: calcNewHeight }
}
return false
}

type Size = {
width: number
height: number
}

const getMaxImgWidth = (img: Size, { width }: Size) => {
let newSize = img
if (img.width > width) {
newSize = { width: width, height: (width * img.height!) / img.width! }
}
if (newSize.height < CROP_SIZE) {
newSize = { width: (CROP_SIZE * newSize.width) / newSize.height, height: CROP_SIZE }
}
return newSize
const getMaxImgWidth = (img: Size) => {
const smallerSideLabel = img.width > img.height ? 'height' : 'width'
const smallerSide = img[smallerSideLabel]
const result = CROP_SIZE / smallerSide
return result
}

function ImageCroper(props: { windowSize: { width: number; height: number }; image: ImageResult; onChange: (image: string) => void }) {
function ImageCroper(props: { windowSize: { width: number; height: number }; image: ImageResult; onChange: (image?: string) => void }) {
const scale = useSharedValue(1)
const startScale = useSharedValue(0)
const translationX = useSharedValue(0)
Expand Down Expand Up @@ -104,24 +88,10 @@ function ImageCroper(props: { windowSize: { width: number; height: number }; ima
useEffect(() => {
;(async () => {
const image = props.image
const maybeNewSize = getMinImgWidthForCrop(image)
if (maybeNewSize) {
const newImg = await manipulateAsync(image.uri, [
{
resize: {
width: maybeNewSize.width,
height: maybeNewSize.height,
},
},
])
originalImage.current = newImg
} else {
originalImage.current = image as ImageResult
}
originalImage.current = image as ImageResult
const orignalSize = { width: originalImage.current.width, height: originalImage.current.height }
setOriginalSizes(orignalSize)
const showSize = getMaxImgWidth(orignalSize, props.windowSize)
const displaySizeRatio = showSize.width / orignalSize.width
const displaySizeRatio = getMaxImgWidth(orignalSize)
scale.value = displaySizeRatio
setReady(true)
})()
Expand Down Expand Up @@ -171,29 +141,35 @@ function ImageCroper(props: { windowSize: { width: number; height: number }; ima
return { x: x > 0 ? x : 0, y: y > 0 ? y : 0, width: CROP_SIZE / scale.value, height: CROP_SIZE / scale.value }
}

const cropImg = () => {
const cropSize = calcCropSize()
const crop = {
originX: cropSize.x,
originY: cropSize.y,
width: cropSize.width,
height: cropSize.height,
}
manipulateAsync(
originalImage.current?.uri || '',
[
{ crop },
{
resize: {
width: 360,
height: 360,
const { mutate, isPending } = useMutation({
mutationFn: (cropSize: { x: number; y: number; width: number; height: number }) => {
const crop = {
originX: cropSize.x,
originY: cropSize.y,
width: cropSize.width,
height: cropSize.height,
}
return manipulateAsync(
originalImage.current?.uri || '',
[
{ crop },
{
resize: {
width: 360,
height: 360,
},
},
},
],
{ compress: 0.5, format: SaveFormat.JPEG, base64: true },
).then((result) => {
],
{ compress: 0.5, format: SaveFormat.JPEG, base64: true },
)
},
onSuccess: (result) => {
props.onChange('data:image/jpeg;base64,' + result.base64!)
})
},
})

const cropImg = () => {
mutate(calcCropSize())
}

const dimentionStyle = media.gtMd ? props.windowSize : {}
Expand All @@ -214,15 +190,26 @@ function ImageCroper(props: { windowSize: { width: number; height: number }; ima
<IngCropperDarkFrame flex={1} />
<XStack>
<IngCropperDarkFrame flex={1} />
<YStack width={CROP_SIZE} height={CROP_SIZE} borderWidth={3} borderColor={'white'} />
<YStack width={CROP_SIZE + 4} height={CROP_SIZE + 4} borderWidth={2} borderColor={'white'} />
<IngCropperDarkFrame flex={1} />
</XStack>
<IngCropperDarkFrame flex={1}>
<YStack gap={16} p={16} justifyContent="center" alignItems="center" position="absolute" bottom={0} right={0}>
<VoxButton theme="yellow" size="lg" onPress={cropImg}>
Terminé
</VoxButton>
</YStack>
<SafeAreaView
style={{
position: 'absolute',
bottom: 0,
right: 0,
}}
>
<XStack gap={16} p={16} justifyContent="center" alignItems="center">
<VoxButton size="lg" onPress={() => props.onChange()} disabled={isPending}>
Annuler
</VoxButton>
<VoxButton theme="gray" inverse={true} size="lg" onPress={cropImg} loading={isPending}>
Enregistrer
</VoxButton>
</XStack>
</SafeAreaView>
</IngCropperDarkFrame>
</YStack>
</ImgCroperOverlayFrame>
Expand All @@ -232,15 +219,15 @@ function ImageCroper(props: { windowSize: { width: number; height: number }; ima
}
const windowSize = Dimensions.get(isWeb ? 'window' : 'screen')

export default function ModalImageCroper(props: { image: ImageResult | null; onClose: (img: string) => void; open: boolean }) {
export default function ModalImageCroper(props: { image: ImageResult | null; onClose: (img?: string) => void; open: boolean }) {
const media = useMedia()
return (
<ModalOrPageBase header={<View />} scrollable={false} open={props.open}>
<ModalOrPageBase header={<View />} scrollable={false} open={props.open} onClose={props.onClose}>
{props.image ? (
media.md ? (
<ImageCroper image={props.image} onChange={props.onClose} windowSize={windowSize} />
) : (
<VoxCard width={600} height={600} overflow="hidden">
<VoxCard width={600} height={600} overflow="hidden" backgroundColor="$gray6">
<ImageCroper
image={props.image}
onChange={props.onClose}
Expand Down
Loading

0 comments on commit 7d66670

Please sign in to comment.