Browse Source
Co-authored-by: Vlad <vlad960706@gmail.com> Reviewed-on: #4 Co-authored-by: Vlad Narizhnyi <vlad960706@gmail.com> Co-committed-by: Vlad Narizhnyi <vlad960706@gmail.com>pull/5/head
91 changed files with 2921 additions and 1536 deletions
Binary file not shown.
@ -1,4 +1,6 @@
@@ -1,4 +1,6 @@
|
||||
rootProject.name = 'Truth' |
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) |
||||
include ':app' |
||||
include ':react-native-iap' |
||||
includeBuild('../node_modules/@react-native/gradle-plugin') |
||||
project(':react-native-iap').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-iap/android') |
Binary file not shown.
@ -1,4 +1,22 @@
@@ -1,4 +1,22 @@
|
||||
export interface CustomPack { |
||||
title: string; |
||||
description: string; |
||||
} |
||||
title: string |
||||
label: string |
||||
description: string |
||||
play: string |
||||
editorBtn: string |
||||
editor: string |
||||
placeholder: string |
||||
addTruth: string |
||||
addDare: string |
||||
viewTruths: string |
||||
viewDares: string |
||||
alertCreateTitle: string |
||||
alertCreateDesc: string |
||||
alertSaveTitle: string |
||||
alertSaveDesc: string |
||||
alertSaveNo: string |
||||
alertSaveYes: string |
||||
alertEmptyTitle: string |
||||
alertEmptyTruthDesc: string |
||||
alertEmptyDaresDesc: string |
||||
} |
||||
|
@ -1,6 +1,10 @@
@@ -1,6 +1,10 @@
|
||||
export interface PurchasesTranslate { |
||||
alertSuccess: string |
||||
descSuccess: string |
||||
alertError: string |
||||
descError: string |
||||
descSuccess: string |
||||
alertError: string |
||||
descError: string |
||||
allPackage: string |
||||
crazy: string |
||||
under18: string |
||||
restore: string |
||||
} |
||||
|
@ -1,5 +1,27 @@
@@ -1,5 +1,27 @@
|
||||
export const customPack = { |
||||
import { CustomPack } from "~i18n/interfaces/custom-pack.interface"; |
||||
|
||||
export const customPack: CustomPack = { |
||||
label: 'Custom package', |
||||
title: 'Create custom pack', |
||||
description: |
||||
'Create your own custom pack with questions and task. It all depends on your imagination!', |
||||
editor: 'Editor', |
||||
placeholder: 'Write here...', |
||||
addTruth: 'Add a truth', |
||||
addDare: 'Add a dare', |
||||
viewTruths: 'View truths', |
||||
viewDares: 'View dares', |
||||
alertCreateTitle: 'Gratefully!', |
||||
alertCreateDesc: 'You can play your custom package now!', |
||||
alertSaveTitle: 'You have unsaved changes', |
||||
alertSaveDesc: 'Save changes?', |
||||
alertSaveNo: 'No', |
||||
alertSaveYes: 'Save', |
||||
alertEmptyTitle: 'Oops!', |
||||
alertEmptyTruthDesc: |
||||
'Your truths list is empty. You need have at least 1 truth', |
||||
alertEmptyDaresDesc: |
||||
'Your dares list is empty. You need have at least 1 dare', |
||||
editorBtn: 'Tasks and questions editor', |
||||
play: 'Play', |
||||
} |
||||
|
@ -1,4 +1,27 @@
@@ -1,4 +1,27 @@
|
||||
export const customPack = { |
||||
title: 'Створіть індивідуальний пакет', |
||||
description: 'Створіть свій власний пакет із запитаннями та завданнями. Все залежить від вашої фантазії!', |
||||
} |
||||
import { CustomPack } from '~i18n/interfaces/custom-pack.interface' |
||||
|
||||
export const customPack: CustomPack = { |
||||
label: 'Власний пакет', |
||||
title: 'Створити власний пакет', |
||||
description: |
||||
'Створіть свій власний пакет з правд та дій. Все залежить від вашої уяви!', |
||||
editor: 'Редактор', |
||||
placeholder: 'Пишіть тут...', |
||||
addTruth: 'Додати питання', |
||||
addDare: 'Додати дію', |
||||
viewTruths: 'Питання', |
||||
viewDares: 'Дії', |
||||
alertCreateTitle: 'Чудово! 🎉', |
||||
alertCreateDesc: 'Зіграйте прямо зараз!', |
||||
alertSaveTitle: 'Ви маєте незбережені зміни', |
||||
alertSaveDesc: 'Зберегти зміни?', |
||||
alertSaveNo: 'Ні', |
||||
alertSaveYes: 'Так', |
||||
alertEmptyTitle: 'Ой! 👀', |
||||
alertEmptyTruthDesc: |
||||
'Ваш список правд порожній. Вам потрібно мати хоча б 1 правду', |
||||
alertEmptyDaresDesc: |
||||
'Ваш список викликів порожній. Вам потрібно мати хоча б 1 виклик', |
||||
editorBtn: 'Редактор правд та дій', |
||||
play: 'Грати', |
||||
} |
||||
|
@ -1,16 +1,14 @@
@@ -1,16 +1,14 @@
|
||||
export enum RouteKey { |
||||
Onboarding = 'Onboarding', |
||||
LanguageSelect = 'LanguageSelect', |
||||
SettingsGroup = 'SettingsGroup', |
||||
Game = 'Game', |
||||
Loading = 'Loading', |
||||
Packages = 'Packages', |
||||
Questions = 'Questions', |
||||
} |
||||
|
||||
export enum SettingsKey { |
||||
TruthOrDare = 'TruthOrDare', |
||||
CustomPackage = 'CustomPackage', |
||||
CustomEditor = 'CustomEditor', |
||||
Settings = 'Settings', |
||||
PrivacyPolicy = 'PrivacyPolicy', |
||||
Purchases = 'Purchases', |
||||
WriteToUs = 'WriteToUs' |
||||
WriteToUs = 'WriteToUs', |
||||
} |
||||
|
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
export enum TypeCustom { |
||||
Questions = 'questions', |
||||
Dares = 'dares', |
||||
} |
@ -1,7 +1,9 @@
@@ -1,7 +1,9 @@
|
||||
import { Language } from "../enums"; |
||||
|
||||
export interface GameItem { |
||||
id: number; |
||||
isDare: boolean; |
||||
en: string; |
||||
ua: string; |
||||
hi: string; |
||||
en: Language; |
||||
ua: Language; |
||||
hi: Language; |
||||
} |
@ -0,0 +1,90 @@
@@ -0,0 +1,90 @@
|
||||
import React, { FC, useEffect, useRef } from 'react' |
||||
import { Animated, LayoutAnimation, StyleSheet } from 'react-native' |
||||
import { TouchableOpacity } from 'react-native-gesture-handler' |
||||
import { Icon, Txt, colors } from '~module/common' |
||||
|
||||
interface IProps { |
||||
text: string |
||||
onDelete: () => void |
||||
delay?: number |
||||
} |
||||
|
||||
export const CustomBlock: FC<IProps> = ({ text, onDelete, delay }) => { |
||||
const deleteAnim = useRef(new Animated.Value(0)).current |
||||
const renderAnim = useRef(new Animated.Value(0)).current |
||||
|
||||
const onPressDelete = () => { |
||||
Animated.spring(deleteAnim, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
}).start() |
||||
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) |
||||
|
||||
setTimeout(onDelete, 200) |
||||
} |
||||
|
||||
useEffect(() => { |
||||
Animated.spring(renderAnim, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
delay, |
||||
}).start() |
||||
}, []) |
||||
|
||||
return ( |
||||
<Animated.View |
||||
style={[ |
||||
styles.block, |
||||
{ |
||||
transform: [ |
||||
{ |
||||
translateX: deleteAnim.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [0, 400], |
||||
}), |
||||
}, |
||||
{ |
||||
translateY: renderAnim.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [-100, 0], |
||||
}), |
||||
}, |
||||
], |
||||
opacity: |
||||
renderAnim || |
||||
deleteAnim.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [1, 0], |
||||
}), |
||||
}, |
||||
]}> |
||||
<Txt style={styles.blockTxt}>{text}</Txt> |
||||
<TouchableOpacity |
||||
style={styles.iconContainer} |
||||
onPress={onPressDelete}> |
||||
<Icon name={'clear'} size={24} color={colors.purple} /> |
||||
</TouchableOpacity> |
||||
</Animated.View> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
block: { |
||||
paddingLeft: 16, |
||||
borderRadius: 20, |
||||
backgroundColor: colors.darkPurple, |
||||
flexDirection: 'row', |
||||
justifyContent: 'space-between', |
||||
alignItems: 'center', |
||||
columnGap: 8, |
||||
}, |
||||
blockTxt: { |
||||
fontSize: 16, |
||||
lineHeight: 24, |
||||
color: colors.purple, |
||||
}, |
||||
iconContainer: { |
||||
padding: 16, |
||||
}, |
||||
}) |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
import React, { FC } from 'react' |
||||
import { StyleSheet, View } from 'react-native' |
||||
import { Font, Txt, colors } from '~module/common' |
||||
|
||||
interface IProps { |
||||
currentText: string |
||||
} |
||||
|
||||
export const EmptyItemsAtom: FC<IProps> = ({ currentText }) => { |
||||
return ( |
||||
<View style={styles.container}> |
||||
<Txt |
||||
style={ |
||||
styles.emptyTxt |
||||
}>{`Your list of ${currentText} is empty`}</Txt> |
||||
</View> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
}, |
||||
|
||||
emptyTxt: { |
||||
fontSize: 22, |
||||
lineHeight: 32, |
||||
fontFamily: Font.Roboto700, |
||||
color: colors.purple, |
||||
marginTop: -50, |
||||
}, |
||||
}) |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './empty-items.atom' |
||||
export * from './custom-block.atom'; |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './screens' |
||||
export * from './atoms' |
@ -0,0 +1,268 @@
@@ -0,0 +1,268 @@
|
||||
import React, { FC, useMemo, useState } from 'react' |
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native' |
||||
import { |
||||
ButtonPrimary, |
||||
Font, |
||||
FormTextControll, |
||||
Header, |
||||
Icon, |
||||
ScreenLayout, |
||||
StorageKey, |
||||
appEvents, |
||||
colors, |
||||
TypeCustom, |
||||
useAppDispatch, |
||||
useNav, |
||||
RouteKey, |
||||
} from '~module/common' |
||||
import { CustomBlock, EmptyItemsAtom } from '../atoms' |
||||
import _ from 'lodash' |
||||
import { useSelector } from 'react-redux' |
||||
import { |
||||
selectCustomPackage, |
||||
selectCustomPackageFromStore, |
||||
setDares, |
||||
setQuestions, |
||||
updateCustomPackage, |
||||
updateCustomPackageFromStore, |
||||
} from '~store/slices' |
||||
import AsyncStorage from '@react-native-async-storage/async-storage' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
export const CustomPackageEditorScreen: FC = () => { |
||||
const dispatch = useAppDispatch() |
||||
const { t } = useTranslation() |
||||
const nav = useNav() |
||||
|
||||
const customPackage = useSelector(selectCustomPackage) |
||||
const customPackageFromStore = useSelector(selectCustomPackageFromStore) |
||||
const isHaveNewChanges = useMemo( |
||||
() => _.isEqual(customPackageFromStore, customPackage), |
||||
[customPackageFromStore, customPackage], |
||||
) |
||||
|
||||
const [value, setValue] = useState('') |
||||
const [activeMod, setActiveMod] = useState(TypeCustom.Questions) |
||||
|
||||
const onView = (mod: TypeCustom) => { |
||||
setActiveMod(mod) |
||||
} |
||||
|
||||
const clearValue = () => { |
||||
setValue('') |
||||
} |
||||
|
||||
const OnAddQuestion = () => { |
||||
if (!value) return |
||||
|
||||
dispatch(setQuestions([...customPackage.questions, value])) |
||||
setActiveMod(TypeCustom.Questions) |
||||
clearValue() |
||||
} |
||||
|
||||
const OnAddDare = () => { |
||||
if (!value) return |
||||
|
||||
dispatch(setDares([...customPackage.dares, value])) |
||||
setActiveMod(TypeCustom.Dares) |
||||
clearValue() |
||||
} |
||||
|
||||
const onDeleteItem = (indexItem: number) => { |
||||
const newCustomItems = customPackage[activeMod].filter( |
||||
(it, index) => index !== indexItem, |
||||
) |
||||
|
||||
activeMod === TypeCustom.Questions |
||||
? dispatch(setQuestions(newCustomItems)) |
||||
: dispatch(setDares(newCustomItems)) |
||||
} |
||||
|
||||
const saveCustomPackage = async () => { |
||||
try { |
||||
const newCustomPackage = JSON.stringify(customPackage) |
||||
|
||||
await AsyncStorage.setItem( |
||||
StorageKey.CustomPackage, |
||||
newCustomPackage, |
||||
) |
||||
|
||||
dispatch(updateCustomPackageFromStore(customPackage)) |
||||
} catch (error) { |
||||
console.error('Error saving custom package:', error) |
||||
} |
||||
} |
||||
|
||||
const onSubmit = () => { |
||||
if (isHaveNewChanges) return |
||||
|
||||
saveCustomPackage() |
||||
|
||||
nav.navigate(RouteKey.CustomPackage) |
||||
} |
||||
|
||||
const goBack = () => { |
||||
if (isHaveNewChanges) return nav.goBack() |
||||
|
||||
appEvents.emit('confirm', { |
||||
title: t('customPack.alertSaveTitle'), |
||||
subtitle: t('customPack.alertSaveDesc'), |
||||
cancelBtnText: t('customPack.alertSaveNo'), |
||||
confirmBtnText: t('customPack.alertSaveYes'), |
||||
isRedButton: true, |
||||
buttons: [ |
||||
{ |
||||
onPress: () => { |
||||
nav.goBack() |
||||
dispatch(updateCustomPackage(customPackageFromStore)) |
||||
}, |
||||
}, |
||||
{ |
||||
onPress: onSubmit, |
||||
}, |
||||
], |
||||
}) |
||||
} |
||||
|
||||
const renderClear = () => ( |
||||
<TouchableOpacity style={styles.clearIcon} onPress={clearValue}> |
||||
<Icon |
||||
name={'clear'} |
||||
size={24} |
||||
color={value ? colors.textPrimary : colors.darkPurple} |
||||
/> |
||||
</TouchableOpacity> |
||||
) |
||||
|
||||
return ( |
||||
<ScreenLayout |
||||
headerComponent={ |
||||
<Header |
||||
title={t('customPack.editor')} |
||||
rightIcon="all_packages" |
||||
onPressLeft={goBack} |
||||
onPressRight={onSubmit} |
||||
colorRightIcon={ |
||||
_.isEqual(customPackageFromStore, customPackage) |
||||
? colors.purple |
||||
: colors.turquoise |
||||
} |
||||
/> |
||||
} |
||||
needScroll |
||||
scrollStyle={{ flexGrow: 1 }}> |
||||
<FormTextControll |
||||
value={value} |
||||
onChange={val => setValue(val)} |
||||
inputProps={{ |
||||
placeholder: t('customPack.placeholder'), |
||||
placeholderTextColor: colors.darkPurple, |
||||
}} |
||||
inputStyle={styles.input} |
||||
renderClearPostfix={renderClear} |
||||
/> |
||||
|
||||
<View style={styles.line} /> |
||||
|
||||
<View style={styles.row}> |
||||
<ButtonPrimary |
||||
mb={0} |
||||
style={styles.btn} |
||||
onPress={OnAddQuestion}> |
||||
{t('customPack.addTruth')} |
||||
</ButtonPrimary> |
||||
<ButtonPrimary mb={0} style={styles.btn} onPress={OnAddDare}> |
||||
{t('customPack.addDare')} |
||||
</ButtonPrimary> |
||||
</View> |
||||
|
||||
<View style={[styles.row, { marginTop: 15 }]}> |
||||
<ButtonPrimary |
||||
styleTxt={styles.txt} |
||||
style={styles.btnTxt} |
||||
txtColor={ |
||||
activeMod === TypeCustom.Questions |
||||
? colors.turquoise |
||||
: colors.purple |
||||
} |
||||
onPress={() => onView(TypeCustom.Questions)}> |
||||
{t('customPack.viewTruths')} |
||||
</ButtonPrimary> |
||||
<ButtonPrimary |
||||
style={styles.btnTxt} |
||||
styleTxt={styles.txt} |
||||
txtColor={ |
||||
activeMod === TypeCustom.Dares |
||||
? colors.turquoise |
||||
: colors.purple |
||||
} |
||||
onPress={() => onView(TypeCustom.Dares)}> |
||||
{t('customPack.viewDares')} |
||||
</ButtonPrimary> |
||||
</View> |
||||
|
||||
{_.isEmpty(customPackage[activeMod]) ? ( |
||||
<EmptyItemsAtom currentText={activeMod} /> |
||||
) : ( |
||||
<> |
||||
<View style={styles.customBody}> |
||||
{customPackage[activeMod].map((it, index) => ( |
||||
<CustomBlock |
||||
key={it} |
||||
text={it} |
||||
delay={index * 100} |
||||
onDelete={() => onDeleteItem(index)} |
||||
/> |
||||
))} |
||||
</View> |
||||
</> |
||||
)} |
||||
</ScreenLayout> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
input: { |
||||
borderRadius: 60, |
||||
backgroundColor: colors.lightPurple, |
||||
borderWidth: 0, |
||||
fontSize: 18, |
||||
color: colors.purple, |
||||
}, |
||||
line: { |
||||
height: 1, |
||||
backgroundColor: colors.lightPurple, |
||||
marginVertical: 24, |
||||
}, |
||||
row: { |
||||
flexDirection: 'row', |
||||
justifyContent: 'space-between', |
||||
columnGap: 15, |
||||
}, |
||||
btn: { |
||||
backgroundColor: colors.darkPurple, |
||||
flex: 1, |
||||
}, |
||||
btnTxt: { |
||||
backgroundColor: null, |
||||
flex: 1, |
||||
borderWidth: 2, |
||||
borderColor: colors.darkPurple, |
||||
}, |
||||
txt: { |
||||
fontSize: 18, |
||||
fontFamily: Font.Roboto700, |
||||
lineHeight: 28, |
||||
}, |
||||
clearIcon: { |
||||
position: 'absolute', |
||||
top: 3, |
||||
right: 10, |
||||
padding: 10, |
||||
}, |
||||
customBody: { |
||||
marginTop: 20, |
||||
marginBottom: 20, |
||||
rowGap: 16, |
||||
}, |
||||
}) |
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
import _ from 'lodash' |
||||
import React, { FC } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { StyleSheet, View } from 'react-native' |
||||
import { useSelector } from 'react-redux' |
||||
import { |
||||
ButtonPrimary, |
||||
Header, |
||||
ProductsEnum, |
||||
RouteKey, |
||||
ScreenLayout, |
||||
appEvents, |
||||
colors, |
||||
useNav, |
||||
} from '~module/common' |
||||
import { purchasesService } from '~module/settings' |
||||
import { selectCustomPackage } from '~store/slices' |
||||
|
||||
export const CustomPackagePreviewScreen: FC = () => { |
||||
const { t } = useTranslation() |
||||
const nav = useNav() |
||||
|
||||
const customPackage = useSelector(selectCustomPackage) |
||||
|
||||
const goToSettings = () => { |
||||
nav.navigate(RouteKey.Settings) |
||||
} |
||||
|
||||
const goToCustomEditor = () => { |
||||
nav.navigate(RouteKey.CustomEditor) |
||||
} |
||||
|
||||
const goToGame = () => { |
||||
const isPurchased = checkIsPurchasedCustomPack() |
||||
|
||||
if (!isPurchased) return nav.navigate(RouteKey.Purchases) |
||||
|
||||
const isFullCustomPack = checkIsFullCustomPack() |
||||
|
||||
if (!isFullCustomPack) return |
||||
|
||||
nav.navigate(RouteKey.Game, { |
||||
packageName: 'My package', |
||||
isCustom: true, |
||||
}) |
||||
} |
||||
|
||||
const checkIsPurchasedCustomPack = () => { |
||||
return purchasesService.purchasedProducts.includes(ProductsEnum.All) |
||||
} |
||||
|
||||
const checkIsFullCustomPack = () => { |
||||
const isEmptyTruths = _.isEmpty(customPackage.questions) |
||||
const isEmptyDares = _.isEmpty(customPackage.dares) |
||||
|
||||
if (isEmptyTruths) { |
||||
appEvents.emit('alert', { |
||||
title: t('customPack.alertEmptyTitle'), |
||||
subtitle: t('customPack.alertEmptyTruthDesc'), |
||||
onClose: () => goToCustomEditor(), |
||||
}) |
||||
return false |
||||
} |
||||
|
||||
if (isEmptyDares) { |
||||
appEvents.emit('alert', { |
||||
title: t('customPack.alertEmptyTitle'), |
||||
subtitle: t('customPack.alertEmptyDaresDesc'), |
||||
onClose: () => goToCustomEditor(), |
||||
}) |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
return ( |
||||
<ScreenLayout |
||||
headerComponent={ |
||||
<Header |
||||
title={t('customPack.label')} |
||||
rightIcon="setting" |
||||
onPressRight={goToSettings} |
||||
/> |
||||
}> |
||||
<View style={styles.container}> |
||||
<ButtonPrimary |
||||
style={styles.editorBtn} |
||||
onPress={goToCustomEditor}> |
||||
{t('customPack.editorBtn')} |
||||
</ButtonPrimary> |
||||
<ButtonPrimary |
||||
style={styles.playBtn} |
||||
iconName="play" |
||||
onPress={goToGame}> |
||||
{t('customPack.play')} |
||||
</ButtonPrimary> |
||||
</View> |
||||
</ScreenLayout> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
rowGap: 24, |
||||
paddingBottom: 50, |
||||
}, |
||||
|
||||
playBtn: { |
||||
flexDirection: 'row', |
||||
columnGap: 8, |
||||
}, |
||||
editorBtn: { |
||||
backgroundColor: colors.darkPurple, |
||||
}, |
||||
}) |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './custom-package-play.screen'; |
||||
export * from './custom-package-editor.screen'; |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from './use-animation-button' |
||||
export * from './use-animation-icons-button.hook'; |
||||
export * from './use-animation-truth-or-dare.hook'; |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
import { useRef } from 'react' |
||||
import { Animated, Easing } from 'react-native' |
||||
import { colors } from '~module/common' |
||||
|
||||
export const useAnimationButton = () => { |
||||
const buttonConfigs = [ |
||||
{ delay: 400, ref: useRef(new Animated.Value(0)) }, |
||||
{ delay: 1200, ref: useRef(new Animated.Value(0)) }, |
||||
{ delay: 800, ref: useRef(new Animated.Value(0)) }, |
||||
] |
||||
|
||||
const startAnimationsBorder = buttonConfigs.map( |
||||
({ delay, ref }) => |
||||
() => |
||||
Animated.timing(ref.current, { |
||||
toValue: 1, |
||||
duration: 600, |
||||
delay, |
||||
easing: Easing.linear, |
||||
useNativeDriver: true, |
||||
}).start(() => ref.current.setValue(0)), |
||||
) |
||||
|
||||
const interpolatedColors = buttonConfigs.map(({ ref }) => |
||||
ref.current.interpolate({ |
||||
inputRange: [0, 0.5, 1], |
||||
outputRange: [colors.darkPurple, colors.blue, colors.darkPurple], |
||||
}), |
||||
) |
||||
|
||||
const animBorderStyles = interpolatedColors.map(color => ({ |
||||
borderColor: color, |
||||
})) |
||||
|
||||
return { |
||||
animBorderStyles, |
||||
startAnimationsBorder, |
||||
} |
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
import { useRef } from 'react' |
||||
import { Animated } from 'react-native' |
||||
|
||||
export const useAnimationIconsButton = () => { |
||||
const rotateIcon = useRef(new Animated.Value(0)).current |
||||
const scaleIcon = useRef(new Animated.Value(0)).current |
||||
|
||||
const animationRotate = () => |
||||
Animated.spring(rotateIcon, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
stiffness: 20, |
||||
}).start(() => rotateIcon.setValue(0)) |
||||
|
||||
const animationScale = () => |
||||
Animated.spring(scaleIcon, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
stiffness: 100, |
||||
}).start(() => scaleIcon.setValue(0)) |
||||
|
||||
const animRotateIconStyle = { |
||||
transform: [ |
||||
{ |
||||
rotate: rotateIcon.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: ['0deg', '360deg'], |
||||
}), |
||||
}, |
||||
], |
||||
} |
||||
|
||||
const animScaleIconStyle = { |
||||
transform: [ |
||||
{ |
||||
scale: scaleIcon.interpolate({ |
||||
inputRange: [0, 0.5, 1], |
||||
outputRange: [1, 1.2, 1], |
||||
}), |
||||
}, |
||||
], |
||||
} |
||||
|
||||
return { |
||||
animRotate: { style: animRotateIconStyle, func: animationRotate }, |
||||
animScale: { style: animScaleIconStyle, func: animationScale }, |
||||
} |
||||
} |
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
import { useRef } from 'react' |
||||
import { Animated } from 'react-native' |
||||
|
||||
export const useAnimationTruthOrDare = () => { |
||||
const animValue = useRef(new Animated.Value(0)).current |
||||
const animText = useRef(new Animated.Value(1)).current |
||||
|
||||
const startAnimation = () => { |
||||
Animated.sequence([ |
||||
Animated.timing(animText, { |
||||
toValue: 0, |
||||
duration: 200, |
||||
useNativeDriver: true, |
||||
}), |
||||
Animated.timing(animText, { |
||||
toValue: 1, |
||||
delay: 400, |
||||
useNativeDriver: true, |
||||
}), |
||||
]).start() |
||||
|
||||
Animated.sequence([ |
||||
Animated.timing(animValue, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
}), |
||||
Animated.timing(animValue, { |
||||
toValue: 0, |
||||
useNativeDriver: true, |
||||
}), |
||||
]).start() |
||||
} |
||||
|
||||
const animStyleContainer = { |
||||
transform: [ |
||||
{ |
||||
translateY: animValue.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [0, 20], |
||||
}), |
||||
}, |
||||
{ |
||||
rotateX: animValue.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: ['0deg', '180deg'], |
||||
}), |
||||
}, |
||||
], |
||||
} |
||||
const animStyleText = { |
||||
transform: [ |
||||
{ |
||||
scale: animText.interpolate({ |
||||
inputRange: [0, 0.5, 0.75, 1], |
||||
outputRange: [0, 0.1, 0.5, 1], |
||||
}), |
||||
}, |
||||
{ |
||||
translateY: animValue.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [0, 250], |
||||
}), |
||||
}, |
||||
], |
||||
} |
||||
|
||||
return { |
||||
startAnimation, |
||||
animStyle: { |
||||
text: animStyleText, |
||||
container: animStyleContainer, |
||||
}, |
||||
} |
||||
} |
@ -1 +1 @@
@@ -1 +1 @@
|
||||
export * from './question-block' |
||||
export * from './truth-or-dare-view' |
||||
|
@ -1,80 +0,0 @@
@@ -1,80 +0,0 @@
|
||||
import { StyleSheet, View, Text } from 'react-native' |
||||
import { colors } from '../../common/colors' |
||||
import React, { useEffect, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { GameItem } from '../../common/typing/interfaces/game-item' |
||||
import { $size, Icon, Txt, useAppDispatch, useAppSelector } from '../../common' |
||||
import { |
||||
getShuffledDares, |
||||
getShuffledTruths, |
||||
getStep, |
||||
resetSteps, |
||||
selectShuffled, |
||||
shuffleItems, |
||||
} from '../../../store/slices' |
||||
|
||||
interface IProps { |
||||
isQuestions: boolean |
||||
} |
||||
|
||||
export const QuestionBlock: React.FC<IProps> = ({ isQuestions }) => { |
||||
const dispatch = useAppDispatch() |
||||
const { i18n } = useTranslation() |
||||
const lang = i18n.language |
||||
|
||||
const currentStep = useAppSelector(getStep) |
||||
|
||||
const shuffledTruths = useAppSelector(getShuffledTruths) |
||||
const shuffledDares = useAppSelector(getShuffledDares) |
||||
|
||||
const gameItems = useAppSelector(selectShuffled) |
||||
|
||||
const dares = gameItems.filter(dare => dare.isDare) |
||||
const questions = gameItems.filter(question => !question.isDare) |
||||
|
||||
console.log(dares) |
||||
|
||||
useEffect(() => { |
||||
if (currentStep >= questions.length) { |
||||
dispatch(resetSteps()) |
||||
dispatch(shuffleItems()) |
||||
} |
||||
if (currentStep >= dares.length) { |
||||
dispatch(resetSteps()) |
||||
dispatch(shuffleItems()) |
||||
} |
||||
}, [currentStep]) |
||||
|
||||
return ( |
||||
<View style={styles.container}> |
||||
<Icon name="magic-star" size={$size(24)} style={styles.starIcon} /> |
||||
<Txt style={styles.text}> |
||||
{currentStep < questions.length && |
||||
isQuestions && |
||||
questions[currentStep][lang as keyof GameItem]} |
||||
{currentStep < dares.length && |
||||
!isQuestions && |
||||
dares[currentStep][lang as keyof GameItem]} |
||||
</Txt> |
||||
</View> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
paddingHorizontal: $size(16), |
||||
paddingVertical: $size(32), |
||||
backgroundColor: colors.darkPurple, |
||||
borderRadius: 20, |
||||
alignItems: 'center', |
||||
}, |
||||
starIcon: { |
||||
marginBottom: $size(14), |
||||
}, |
||||
text: { |
||||
color: colors.purple, |
||||
fontSize: $size(22), |
||||
lineHeight: $size(32), |
||||
textAlign: 'center', |
||||
}, |
||||
}) |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
import React, { useEffect, useMemo } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { |
||||
Font, |
||||
Icon, |
||||
Language, |
||||
TypeCustom, |
||||
colors, |
||||
useAppDispatch, |
||||
useAppSelector, |
||||
} from '../../common' |
||||
import { |
||||
getStep, |
||||
resetSteps, |
||||
shuffleCustom, |
||||
shuffleItems, |
||||
} from '../../../store/slices' |
||||
import { Animated, StyleSheet } from 'react-native' |
||||
import { useAnimationTruthOrDare } from '../animations' |
||||
|
||||
interface IProps { |
||||
items: any[] |
||||
isCustom?: TypeCustom | undefined |
||||
} |
||||
|
||||
export const TruthOrDareView: React.FC<IProps> = ({ items, isCustom }) => { |
||||
const dispatch = useAppDispatch() |
||||
const { i18n } = useTranslation() |
||||
const currentStep = useAppSelector(getStep) |
||||
const memoItems = useMemo(() => items, [items]) |
||||
|
||||
const { startAnimation, animStyle } = useAnimationTruthOrDare() |
||||
|
||||
useEffect(() => { |
||||
if (currentStep == memoItems.length) { |
||||
dispatch(shuffleItems()) |
||||
dispatch(resetSteps()) |
||||
isCustom && dispatch(shuffleCustom()) |
||||
} |
||||
|
||||
startAnimation() |
||||
}, [currentStep]) |
||||
|
||||
if (currentStep === 0) { |
||||
startAnimation() |
||||
} |
||||
|
||||
const content = isCustom |
||||
? memoItems?.[currentStep] |
||||
: memoItems?.[currentStep]?.[i18n.language as Language] |
||||
|
||||
return ( |
||||
<Animated.View style={[styles.container, animStyle.container]}> |
||||
<Icon |
||||
name="magic-star" |
||||
size={24} |
||||
color={colors.turquoise} |
||||
style={styles.starIcon} |
||||
/> |
||||
<Animated.Text style={[styles.text, animStyle.text]}> |
||||
{content} |
||||
</Animated.Text> |
||||
</Animated.View> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
paddingHorizontal: 16, |
||||
paddingVertical: 32, |
||||
backgroundColor: colors.darkPurple, |
||||
borderRadius: 20, |
||||
alignItems: 'center', |
||||
marginTop: '50%', |
||||
}, |
||||
starIcon: { |
||||
marginBottom: 14, |
||||
}, |
||||
text: { |
||||
color: colors.purple, |
||||
fontSize: 22, |
||||
lineHeight: 32, |
||||
textAlign: 'center', |
||||
fontFamily: Font.Roboto400, |
||||
}, |
||||
}) |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
import { selectShuffled, selectShuffledCustom } from '../../../store/slices' |
||||
import { TypeCustom, useAppSelector } from '../../common' |
||||
|
||||
interface UseTruthOrDareProps { |
||||
isTruth: boolean |
||||
customType: TypeCustom |
||||
} |
||||
|
||||
export const getCurrentTruthOrDare = ({ |
||||
isTruth, |
||||
customType, |
||||
}: UseTruthOrDareProps) => { |
||||
const gameItems = useAppSelector(selectShuffled) |
||||
const customPackage = useAppSelector(selectShuffledCustom) |
||||
const dares = gameItems.filter(dare => dare.isDare) |
||||
const questions = gameItems.filter(question => !question.isDare) |
||||
const packageTruthOrDare = isTruth ? questions : dares |
||||
|
||||
const truthOrDareItems = customType |
||||
? customPackage[customType] |
||||
: packageTruthOrDare |
||||
|
||||
return truthOrDareItems |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './get-current-truth-dares.helper' |
@ -1,2 +1,4 @@
@@ -1,2 +1,4 @@
|
||||
export * from './screens' |
||||
export * from './components' |
||||
export * from './animations'; |
||||
export * from './helper'; |
||||
|
@ -1,2 +1,2 @@
@@ -1,2 +1,2 @@
|
||||
export * from './game.screen' |
||||
export * from './questions.screen' |
||||
export * from './truth-or-dare.screen' |
||||
|
@ -1,78 +0,0 @@
@@ -1,78 +0,0 @@
|
||||
import React from 'react' |
||||
import { StyleSheet, View } from 'react-native' |
||||
import { useRoute } from '@react-navigation/native' |
||||
import { |
||||
$size, |
||||
ButtonWithIcon, |
||||
Header, |
||||
RouteKey, |
||||
ScreenLayout, |
||||
Txt, |
||||
useAppDispatch, |
||||
useNav, |
||||
} from '../../common' |
||||
import { nextStep, resetSteps, shuffleItems } from '../../../store/slices' |
||||
import { QuestionBlock } from '../components' |
||||
|
||||
export const QuestionsScreen: React.FC = () => { |
||||
const nav = useNav() |
||||
const { params }: any = useRoute() |
||||
|
||||
const { packageName, isQuestions } = params |
||||
|
||||
const dispatch = useAppDispatch() |
||||
|
||||
const goBack = () => { |
||||
nav.navigate(RouteKey.Game, { packageName }) |
||||
} |
||||
|
||||
const goGameScreen = () => { |
||||
nav.navigate(RouteKey.Packages) |
||||
dispatch(nextStep()) |
||||
} |
||||
|
||||
const refreshList = () => { |
||||
dispatch(shuffleItems()) |
||||
dispatch(resetSteps()) |
||||
} |
||||
|
||||
return ( |
||||
<ScreenLayout |
||||
headerComponent={ |
||||
<Header |
||||
gamer |
||||
onPressLeft={() => goBack()} |
||||
title={packageName} |
||||
/> |
||||
}> |
||||
<View style={{ flex: 1 }}> |
||||
<Txt mod="xxl" style={styles.playerName}> |
||||
Player |
||||
</Txt> |
||||
|
||||
<QuestionBlock isQuestions={isQuestions} /> |
||||
|
||||
<View style={styles.buttons}> |
||||
<ButtonWithIcon iconName="union" onPress={refreshList} /> |
||||
<ButtonWithIcon iconName="play" onPress={goGameScreen} /> |
||||
<ButtonWithIcon iconName="add-plus" onPress={() => null} /> |
||||
</View> |
||||
</View> |
||||
</ScreenLayout> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
playerName: { |
||||
fontWeight: '700', |
||||
textAlign: 'center', |
||||
marginBottom: $size(88), |
||||
}, |
||||
buttons: { |
||||
alignItems: 'center', |
||||
flexDirection: 'row', |
||||
justifyContent: 'space-between', |
||||
marginBottom: $size(66), |
||||
marginTop: 'auto', |
||||
}, |
||||
}) |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
import React, { useRef } from 'react' |
||||
import { Animated, StyleSheet, View } from 'react-native' |
||||
import { useRoute } from '@react-navigation/native' |
||||
import { |
||||
ButtonWithIcon, |
||||
Header, |
||||
RouteKey, |
||||
ScreenLayout, |
||||
TypeCustom, |
||||
useAppDispatch, |
||||
useAppSelector, |
||||
useNav, |
||||
} from '../../common' |
||||
import { |
||||
nextStep, |
||||
resetSteps, |
||||
selectShuffled, |
||||
selectShuffledCustom, |
||||
shuffleCustom, |
||||
shuffleItems, |
||||
} from '../../../store/slices' |
||||
import { TruthOrDareView } from '../components' |
||||
import { useAnimationIconsButton } from '../animations' |
||||
import { getCurrentTruthOrDare } from '../helper' |
||||
|
||||
interface IRouteParams { |
||||
packageName?: string |
||||
isTruth?: boolean |
||||
customType?: TypeCustom |
||||
} |
||||
|
||||
export const TruthOrDareScreen: React.FC = () => { |
||||
const nav = useNav() |
||||
const dispatch = useAppDispatch() |
||||
const { params } = useRoute() |
||||
const { packageName, isTruth, customType }: IRouteParams = params |
||||
|
||||
const truthOrDareItems = getCurrentTruthOrDare({ isTruth, customType }) |
||||
const { animRotate, animScale } = useAnimationIconsButton() |
||||
|
||||
const goBack = () => { |
||||
nav.navigate(RouteKey.Game, { packageName }) |
||||
} |
||||
|
||||
const onNext = () => { |
||||
dispatch(nextStep()) |
||||
} |
||||
|
||||
const refreshList = () => { |
||||
!customType && dispatch(shuffleItems()) |
||||
customType && dispatch(shuffleCustom()) |
||||
dispatch(resetSteps()) |
||||
} |
||||
|
||||
return ( |
||||
<ScreenLayout |
||||
headerComponent={ |
||||
<Header |
||||
gamer |
||||
onPressLeft={() => goBack()} |
||||
title={packageName} |
||||
/> |
||||
}> |
||||
<View style={{ flex: 1 }}> |
||||
<TruthOrDareView |
||||
items={truthOrDareItems} |
||||
isCustom={customType} |
||||
/> |
||||
|
||||
<View style={styles.buttons}> |
||||
<ButtonWithIcon |
||||
styleBtn={{ width: 101 }} |
||||
iconName="union" |
||||
onPress={refreshList} |
||||
animation={animRotate.func} |
||||
animStyle={animRotate.style} |
||||
/> |
||||
|
||||
<ButtonWithIcon |
||||
styleBtn={{ width: 101 }} |
||||
iconName="play" |
||||
onPress={onNext} |
||||
animation={animScale.func} |
||||
animStyle={animScale.style} |
||||
/> |
||||
</View> |
||||
</View> |
||||
</ScreenLayout> |
||||
) |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
buttons: { |
||||
alignItems: 'center', |
||||
flexDirection: 'row', |
||||
justifyContent: 'space-between', |
||||
marginBottom: 66, |
||||
marginTop: 'auto', |
||||
}, |
||||
}) |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './use-animation-list.hook'; |
||||
export * from './use-animation-custom-item.hook'; |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useRef } from 'react' |
||||
import { Animated } from 'react-native' |
||||
|
||||
export const useAnimationCustomItem = () => { |
||||
const animValue = useRef(new Animated.Value(0)).current |
||||
const scaleAnim = useRef(new Animated.Value(0)).current |
||||
|
||||
const startAnimation = () => { |
||||
Animated.sequence([ |
||||
Animated.timing(scaleAnim, { |
||||
toValue: 1, |
||||
duration: 100, |
||||
useNativeDriver: true, |
||||
}), |
||||
Animated.timing(scaleAnim, { |
||||
toValue: 0, |
||||
duration: 300, |
||||
useNativeDriver: true, |
||||
}), |
||||
]).start() |
||||
} |
||||
|
||||
useEffect(() => { |
||||
Animated.spring(animValue, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
delay: 1200, |
||||
}).start() |
||||
}, []) |
||||
|
||||
const animationStyleItem = { |
||||
transform: [ |
||||
{ |
||||
translateY: animValue.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [300, 0], |
||||
}), |
||||
}, |
||||
{ |
||||
scale: scaleAnim.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [1, 1.2], |
||||
}), |
||||
}, |
||||
], |
||||
opacity: scaleAnim.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [1, 0.1], |
||||
}), |
||||
} |
||||
|
||||
return { animationStyleItem, startAnimation } |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
import React, { useEffect, useRef } from 'react' |
||||
import { Animated, Easing } from 'react-native' |
||||
import { useIsFocused } from '@react-navigation/native' |
||||
|
||||
export const useAnimationList = (delay: number) => { |
||||
const isFocus = useIsFocused() |
||||
|
||||
const animTransformX = useRef(new Animated.Value(0)).current |
||||
const animTransformY = useRef(new Animated.Value(0)).current |
||||
|
||||
useEffect(() => { |
||||
const animation = Animated.parallel([ |
||||
Animated.spring(animTransformX, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
delay, |
||||
}), |
||||
Animated.loop( |
||||
Animated.sequence([ |
||||
Animated.timing(animTransformY, { |
||||
toValue: -3, |
||||
duration: 2000, |
||||
delay, |
||||
useNativeDriver: true, |
||||
easing: Easing.linear, |
||||
}), |
||||
Animated.timing(animTransformY, { |
||||
toValue: 0, |
||||
duration: 2000, |
||||
delay, |
||||
useNativeDriver: true, |
||||
easing: Easing.linear, |
||||
}), |
||||
]), |
||||
), |
||||
]) |
||||
|
||||
if (!isFocus) { |
||||
return animation.reset() |
||||
} |
||||
|
||||
animation.start() |
||||
}, [isFocus]) |
||||
|
||||
const animationStyleItem = { |
||||
opacity: animTransformX, |
||||
transform: [ |
||||
{ |
||||
translateX: animTransformX.interpolate({ |
||||
inputRange: [0, 1], |
||||
outputRange: [-100, 0], |
||||
}), |
||||
}, |
||||
{ |
||||
translateY: animTransformY, |
||||
}, |
||||
], |
||||
} |
||||
|
||||
return { animationStyleItem } |
||||
} |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
import React, { FC, useEffect, useRef } from 'react' |
||||
import { Animated, StyleSheet, View } from 'react-native' |
||||
import { colors, Icon } from '../../common' |
||||
import { useIsFocused } from '@react-navigation/native' |
||||
|
||||
interface IProps {} |
||||
|
||||
export const AnimatedDiamondIcon: FC<IProps> = ({}) => { |
||||
const rotateIcon = useRef(new Animated.Value(0)).current |
||||
const isFocus = useIsFocused() |
||||
|
||||
useEffect(() => { |
||||
const loopAnimation = Animated.loop( |
||||
Animated.sequence([ |
||||
Animated.spring(rotateIcon, { |
||||
toValue: 1, |
||||
useNativeDriver: true, |
||||
stiffness: 5, |
||||
}), |
||||
]), |
||||
) |
||||
|
||||
if (!isFocus) { |
||||
return loopAnimation.stop() |
||||
} |
||||
|
||||
loopAnimation.start() |
||||
}, [isFocus]) |
||||
|
||||
return ( |
||||
<View style={styles.iconContainer}> |
||||
<Animated.View |
||||
style={{ |
||||
transform: [ |
||||
{ |
||||
rotateY: rotateIcon.interpolate({ |
||||
inputRange: [0, 0.5, 1], |
||||
outputRange: ['0deg', '360deg', '720deg'], |
||||
}), |
||||
}, |
||||
{ |
||||
rotate: rotateIcon.interpolate({ |
||||
inputRange: [0, 0.5, 1], |
||||
outputRange: ['0deg', '15deg', '0deg'], |
||||
}), |
||||
}, |
||||
], |
||||
}}> |
||||
<Icon name={'diamond'} size={24} color={colors.turquoise} /> |
||||
</Animated.View> |
||||
</View> |
||||
) |
||||
} |
||||
const styles = StyleSheet.create({ |
||||
iconContainer: { |
||||
backgroundColor: colors.primaryColor, |
||||
width: 40, |
||||
height: 40, |
||||
borderRadius: 10, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
marginRight: 16, |
||||
}, |
||||
}) |
@ -1,55 +1,85 @@
@@ -1,55 +1,85 @@
|
||||
import React from 'react' |
||||
import React, { FC } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native' |
||||
import { $size, colors, Icon, Txt } from '../../common' |
||||
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native' |
||||
import { colors, Font, RouteKey, Txt, useNav } from '../../common' |
||||
import { AnimatedDiamondIcon } from './animated-diamond-icon.atom' |
||||
import { useAnimationCustomItem } from '../animation' |
||||
|
||||
export const CustomPackage = () => { |
||||
interface IProps {} |
||||
|
||||
export const CustomPackage: FC<IProps> = ({}) => { |
||||
const { t } = useTranslation() |
||||
const nav = useNav() |
||||
const { startAnimation, animationStyleItem } = useAnimationCustomItem() |
||||
|
||||
const onPressCustomPackage = () => { |
||||
startAnimation() |
||||
nav.navigate(RouteKey.CustomPackage) |
||||
} |
||||
|
||||
return ( |
||||
<TouchableOpacity style={style.container}> |
||||
<View style={{ flexDirection: 'row' }}> |
||||
<View style={style.iconContainer}> |
||||
<Icon name="lock" size={$size(24)} color="white" /> |
||||
</View> |
||||
<View style={style.textContainer}> |
||||
<Txt style={style.title}>{t('customPack.title')}</Txt> |
||||
<Txt style={style.description}> |
||||
<Animated.View style={[styles.container, animationStyleItem]}> |
||||
<TouchableOpacity |
||||
style={{ flexDirection: 'row' }} |
||||
onPress={onPressCustomPackage}> |
||||
<AnimatedDiamondIcon /> |
||||
<View style={styles.textContainer}> |
||||
<Txt |
||||
style={styles.title} |
||||
font={Font.Roboto700} |
||||
color={colors.turquoise}> |
||||
{t('customPack.title')} |
||||
</Txt> |
||||
<Txt style={styles.description} color={colors.textPrimary}> |
||||
{t('customPack.description')} |
||||
</Txt> |
||||
</View> |
||||
</View> |
||||
</TouchableOpacity> |
||||
</TouchableOpacity> |
||||
</Animated.View> |
||||
) |
||||
} |
||||
const style = StyleSheet.create({ |
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
backgroundColor: '#99EDCC', |
||||
backgroundColor: colors.lightPurple, |
||||
borderRadius: 20, |
||||
padding: $size(16), |
||||
padding: 16, |
||||
overflow: 'hidden', |
||||
}, |
||||
textContainer: { |
||||
flex: 1, |
||||
marginTop: -4, |
||||
}, |
||||
title: { |
||||
fontWeight: '600', |
||||
fontSize: $size(22), |
||||
lineHeight: $size(32), |
||||
color: colors.darkPurple, |
||||
fontSize: 22, |
||||
lineHeight: 32, |
||||
marginBottom: 4, |
||||
}, |
||||
description: { |
||||
fontSize: $size(16), |
||||
lineHeight: $size(24), |
||||
color: colors.darkPurple, |
||||
fontSize: 16, |
||||
lineHeight: 24, |
||||
}, |
||||
iconContainer: { |
||||
backgroundColor: colors.primaryColor, |
||||
padding: $size(10), |
||||
width: $size(50), |
||||
height: $size(50), |
||||
borderRadius: 20, |
||||
width: 40, |
||||
height: 40, |
||||
borderRadius: 10, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
marginRight: $size(16), |
||||
marginRight: 16, |
||||
}, |
||||
price: { |
||||
position: 'absolute', |
||||
top: 5, |
||||
right: -43, |
||||
height: 35, |
||||
width: 130, |
||||
transform: [{ rotate: '45deg' }], |
||||
backgroundColor: colors.red, |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
}, |
||||
priceTxt: { |
||||
fontSize: 16, |
||||
lineHeight: 24, |
||||
}, |
||||
}) |
||||
|
@ -1,2 +1,3 @@
@@ -1,2 +1,3 @@
|
||||
export * from './create-custom-package.atom' |
||||
export * from './packages-page-separator.atom' |
||||
export * from './animated-diamond-icon.atom' |
||||
|
@ -1,21 +1,19 @@
@@ -1,21 +1,19 @@
|
||||
import React from 'react' |
||||
import { StyleSheet } from 'react-native' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { $size, Txt, colors } from '../../common' |
||||
import { Txt, colors } from '../../common' |
||||
|
||||
export const SelectedLanguage = () => { |
||||
const { i18n } = useTranslation() |
||||
|
||||
console.log(i18n.language) |
||||
|
||||
return <Txt style={styles.text}>{i18n.language}</Txt> |
||||
} |
||||
const styles = StyleSheet.create({ |
||||
text: { |
||||
color: colors.purple, |
||||
fontWeight: '600', |
||||
fontSize: $size(18), |
||||
lineHeight: $size(28), |
||||
fontSize: 18, |
||||
lineHeight: 28, |
||||
textTransform: 'uppercase', |
||||
}, |
||||
}) |
||||
|
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit' |
||||
import { StorageKey, TypeCustom } from '../../module/common' |
||||
import { RootState } from '../store' |
||||
import _ from 'lodash' |
||||
import AsyncStorage from '@react-native-async-storage/async-storage' |
||||
|
||||
interface ICustomPackage { |
||||
[TypeCustom.Questions]: string[] |
||||
[TypeCustom.Dares]: string[] |
||||
} |
||||
|
||||
const defaultCustomPackage: ICustomPackage = { |
||||
[TypeCustom.Questions]: [], |
||||
[TypeCustom.Dares]: [], |
||||
} |
||||
|
||||
export interface CustomPackageState { |
||||
customPackageFromStore: ICustomPackage |
||||
customPackage: ICustomPackage |
||||
shuffleCustomPackage: ICustomPackage |
||||
shuffled: ICustomPackage |
||||
loaded: boolean |
||||
hasError: boolean |
||||
} |
||||
|
||||
const initialState: CustomPackageState = { |
||||
customPackage: defaultCustomPackage, |
||||
shuffleCustomPackage: defaultCustomPackage, |
||||
customPackageFromStore: defaultCustomPackage, |
||||
shuffled: defaultCustomPackage, |
||||
loaded: false, |
||||
hasError: false, |
||||
} |
||||
|
||||
export const getCustomPackage = createAsyncThunk( |
||||
'get-custom-package', |
||||
async () => { |
||||
const response = await AsyncStorage.getItem(StorageKey.CustomPackage) |
||||
|
||||
return response ? JSON.parse(response) : defaultCustomPackage |
||||
}, |
||||
) |
||||
|
||||
export const customPackageSlice = createSlice({ |
||||
name: 'customPackage', |
||||
initialState, |
||||
reducers: { |
||||
shuffleCustom: state => { |
||||
const shuffleTruths = _.shuffle(state.customPackage.questions) |
||||
const shuffleDares = _.shuffle(state.customPackage.dares) |
||||
|
||||
state.shuffleCustomPackage = { |
||||
dares: shuffleDares, |
||||
questions: shuffleTruths, |
||||
} |
||||
}, |
||||
setQuestions: (state, action: PayloadAction<string[]>) => { |
||||
const updateCustomPackage = { |
||||
...state.customPackage, |
||||
questions: action.payload, |
||||
} |
||||
state.customPackage = updateCustomPackage |
||||
}, |
||||
setDares: (state, action: PayloadAction<string[]>) => { |
||||
const updateCustomPackage = { |
||||
...state.customPackage, |
||||
dares: action.payload, |
||||
} |
||||
state.customPackage = updateCustomPackage |
||||
}, |
||||
updateCustomPackage: (state, action: PayloadAction<ICustomPackage>) => { |
||||
state.customPackage = action.payload |
||||
}, |
||||
updateCustomPackageFromStore: ( |
||||
state, |
||||
action: PayloadAction<ICustomPackage>, |
||||
) => { |
||||
state.customPackageFromStore = action.payload |
||||
}, |
||||
}, |
||||
extraReducers(builder) { |
||||
builder |
||||
.addCase(getCustomPackage.fulfilled, (state, action) => { |
||||
state.customPackage = action.payload |
||||
state.customPackageFromStore = action.payload |
||||
state.hasError = false |
||||
state.loaded = true |
||||
}) |
||||
.addCase(getCustomPackage.pending, state => { |
||||
state.loaded = false |
||||
}) |
||||
.addCase(getCustomPackage.rejected, state => { |
||||
state.hasError = true |
||||
}) |
||||
}, |
||||
}) |
||||
|
||||
export const { |
||||
setQuestions, |
||||
setDares, |
||||
updateCustomPackageFromStore, |
||||
updateCustomPackage, |
||||
shuffleCustom, |
||||
} = customPackageSlice.actions |
||||
|
||||
export const selectCustomPackage = (state: RootState) => |
||||
state.customPackage.customPackage |
||||
export const selectCustomPackageFromStore = (state: RootState) => |
||||
state.customPackage.customPackageFromStore |
||||
export const selectCustomLoaded = (state: RootState) => |
||||
state.customPackage.loaded |
||||
export const selectShuffledCustom = (state: RootState) => |
||||
state.customPackage.shuffleCustomPackage |
||||
|
||||
export default customPackageSlice.reducer |
@ -1,52 +0,0 @@
@@ -1,52 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit' |
||||
import { Dare } from '../../module/common' |
||||
import _ from 'lodash' |
||||
import { RootState } from '../store' |
||||
|
||||
interface DaresState { |
||||
dares: Dare[] |
||||
shuffledDares: Dare[] |
||||
} |
||||
|
||||
const initialState: DaresState = { |
||||
dares: [ |
||||
{ |
||||
id: 0, |
||||
dare: 'Дія 1', |
||||
}, |
||||
{ |
||||
id: 1, |
||||
dare: 'Дія 2', |
||||
}, |
||||
{ |
||||
id: 2, |
||||
dare: 'Дія 3', |
||||
}, |
||||
{ |
||||
id: 3, |
||||
dare: 'Дія 4', |
||||
}, |
||||
{ |
||||
id: 4, |
||||
dare: 'Дія 5', |
||||
}, |
||||
], |
||||
shuffledDares: [], |
||||
} |
||||
|
||||
export const DaresSlice = createSlice({ |
||||
name: 'dares', |
||||
initialState, |
||||
reducers: { |
||||
shuffleDares: state => { |
||||
state.shuffledDares = _.shuffle(state.dares) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
export const { shuffleDares } = DaresSlice.actions |
||||
|
||||
export const getDares = (state: RootState) => state.dares.dares |
||||
export const getShuffledDares = (state: RootState) => state.dares.shuffledDares |
||||
|
||||
export default DaresSlice.reducer |
@ -1,4 +1,3 @@
@@ -1,4 +1,3 @@
|
||||
export * from './current-step-slice' |
||||
export * from './dares-slice' |
||||
export * from './posts.slice' |
||||
export * from './truth-slice' |
||||
export * from './custom-package.slice'; |
||||
|
@ -1,53 +0,0 @@
@@ -1,53 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit' |
||||
|
||||
import { RootState } from '../store' |
||||
import { Truth } from '../../module/common' |
||||
import _ from 'lodash' |
||||
|
||||
interface TruthsState { |
||||
truths: Truth[] |
||||
shuffledTruth: Truth[] |
||||
} |
||||
|
||||
const initialState: TruthsState = { |
||||
truths: [ |
||||
{ |
||||
id: 0, |
||||
question: 'Питання 1', |
||||
}, |
||||
{ |
||||
id: 1, |
||||
question: 'Питання 2', |
||||
}, |
||||
{ |
||||
id: 2, |
||||
question: 'Питання 3', |
||||
}, |
||||
{ |
||||
id: 3, |
||||
question: 'Питання 4', |
||||
}, |
||||
{ |
||||
id: 4, |
||||
question: 'Питання 5', |
||||
}, |
||||
], |
||||
shuffledTruth: [], |
||||
} |
||||
|
||||
export const TruthSlice = createSlice({ |
||||
name: 'truth', |
||||
initialState, |
||||
reducers: { |
||||
shuffleTruths: state => { |
||||
state.shuffledTruth = _.shuffle(state.truths) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
export const { shuffleTruths } = TruthSlice.actions |
||||
|
||||
export const getTruths = (state: RootState) => state.truth.truths |
||||
export const getShuffledTruths = (state: RootState) => state.truth.shuffledTruth |
||||
|
||||
export default TruthSlice.reducer |
Loading…
Reference in new issue