diff --git a/storybook/stories/FlipCreditCard/CardView.tsx b/storybook/stories/FlipCreditCard/CardView.tsx new file mode 100644 index 0000000..8638912 --- /dev/null +++ b/storybook/stories/FlipCreditCard/CardView.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { StyleProp, ViewStyle } from "react-native"; +import { + defaultCVCPlaceholder, + defaultExpiryPlaceholder, + defaultCardHolderNamePlaceholder, + defaultCreditCardNumberPlaceholder +} from "./common"; +import CreditCardViewFront from "./CardViewFront"; +import CreditCardViewBack from "./CardViewBack"; + +export interface CreditCardLabels { + expiryDate: string; +} + +export interface CreditCardFields { + creditCardName: string; + creditCardNumber: string; + expiryDate: string; + cvc: string; +} + +export interface Props { + style?: StyleProp; + scale?: number; + creditCardName?: string; + creditCardNumber?: string; + creditCardNumberLength?: number; + expiryDate?: string; + cvc?: string; + placeholders?: Partial; + localizedLabels?: Partial; + visibleFace: "front" | "back"; +} + +export default function CardView(props: Props) { + const { + creditCardName, + creditCardNumber, + cvc, + placeholders: optionalPlaceholders, + localizedLabels: optionalLabels, + expiryDate, + visibleFace, + style: externalStyle + } = props; + + const placeholders = { + cvc: defaultCVCPlaceholder, + expiryDate: defaultExpiryPlaceholder, + creditCardName: defaultCardHolderNamePlaceholder, + creditCardNumber: defaultCreditCardNumberPlaceholder, + ...optionalPlaceholders + }; + + const labels = { + expiryDate: "MM/YY", + ...optionalLabels + }; + + if (visibleFace === "front") { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/storybook/stories/FlipCreditCard/CardViewBack.tsx b/storybook/stories/FlipCreditCard/CardViewBack.tsx new file mode 100644 index 0000000..c6cf6b0 --- /dev/null +++ b/storybook/stories/FlipCreditCard/CardViewBack.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { View, Text, StyleProp, ViewStyle } from "react-native"; +import style, { + CardBackgroundColorStart, + CardBackgroundColorEnd +} from "./style"; +import { CardInformationContainer } from "./CardViewFront"; +import { LinearGradient } from "expo"; +import { addPlaceholderStyleIfNeeded } from "./common"; +import { CreditCardFields, CreditCardLabels } from "./CardView"; + +interface CreditCardViewBackProps { + cvc: string; + cardHolderName: string; + expiryDate: string; + style?: StyleProp; + placeholders: CreditCardFields; + labels: CreditCardLabels; +} + +export default function CreditCardViewBack(props: CreditCardViewBackProps) { + const { + cvc, + cardHolderName, + expiryDate, + style: externalStyle, + placeholders, + labels + } = props; + + return ( + + + + + {cvc || placeholders.cvc} + + + + + ); +} diff --git a/storybook/stories/FlipCreditCard/CardViewFront.tsx b/storybook/stories/FlipCreditCard/CardViewFront.tsx new file mode 100644 index 0000000..4c7f2a5 --- /dev/null +++ b/storybook/stories/FlipCreditCard/CardViewFront.tsx @@ -0,0 +1,126 @@ +import React from "react"; +import { + View, + Text, + Image, + StyleProp, + ViewStyle, + TextStyle +} from "react-native"; +import { LinearGradient } from "expo"; +import style, { + CardBackgroundColorEnd, + CardBackgroundColorStart +} from "./style"; +import { addPlaceholderStyleIfNeeded } from "./common"; +import { CreditCardFields, CreditCardLabels } from "./CardView"; + +interface CreditCardViewFrontProps { + cardHolderName: string; + number: string; + expiryDate: string; + style?: StyleProp; + placeholders: CreditCardFields; + labels: CreditCardLabels; +} + +interface CreditCardInfoContainerProps { + cardHolderName: string; + expiryDate: string; + textStyle?: StyleProp; + style?: StyleProp; + placeholders: CreditCardFields; + labels: CreditCardLabels; +} + +export function CardInformationContainer(props: CreditCardInfoContainerProps) { + const { + cardHolderName, + expiryDate, + textStyle, + style: externalStyle, + placeholders, + labels + } = props; + + return ( + + + {(cardHolderName || placeholders.creditCardName).toUpperCase()} + + + + {labels.expiryDate} + + + {expiryDate || placeholders.expiryDate} + + + + ); +} + +export default function CreditCardViewFront(props: CreditCardViewFrontProps) { + const { + cardHolderName, + number, + expiryDate, + placeholders, + style: externalStyle, + labels + } = props; + + return ( + + + + + + + {number || placeholders.creditCardNumber} + + + ); +} diff --git a/storybook/stories/FlipCreditCard/FlipOnGesture.tsx b/storybook/stories/FlipCreditCard/FlipOnGesture.tsx new file mode 100644 index 0000000..e2600e4 --- /dev/null +++ b/storybook/stories/FlipCreditCard/FlipOnGesture.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import CardView, { Props as CardViewProps } from "./CardView"; +import { + Animated, + TouchableOpacity, + View, + PanResponder, + Dimensions +} from "react-native"; +import style from "./style"; + +interface Props extends CardViewProps {} + +type Face = "front" | "back"; + +interface State { + visibleFace: Face; +} + +const screenWidth = Dimensions.get("window").width; + +export default class FlipCreditCard extends React.PureComponent { + state = { visibleFace: "front" as Face }; + prevX = 0; + rotation = new Animated.Value(0); + currentRotation = 0; + + _panResponder = PanResponder.create({ + onStartShouldSetPanResponder: (evt, gestureState) => true, + onStartShouldSetPanResponderCapture: (evt, gestureState) => true, + onMoveShouldSetPanResponder: (evt, gestureState) => true, + onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, + + onPanResponderGrant: (evt, gestureState) => { + // The gesture has started. Show visual feedback so the user knows + // what is happening! + // gestureState.d{x,y} will be set to zero now + this.prevX = 0; + }, + onPanResponderMove: (evt, gestureState) => { + // The most recent move distance is gestureState.move{X,Y} + // The accumulated gesture distance since becoming responder is + // gestureState.d{x,y} + this.currentRotation = Math.max( + 0, + Math.min( + 1, + this.currentRotation + (gestureState.dx - this.prevX) / screenWidth + ) + ); + + this.prevX = gestureState.dx; + + if (this.currentRotation >= 0.5) { + this.setState({ visibleFace: "back" }); + } else { + this.setState({ visibleFace: "front" }); + } + + this.rotation.setValue(this.currentRotation); + }, + onShouldBlockNativeResponder: (evt, gestureState) => { + // Returns whether this component should block native components from becoming the JS + // responder. Returns true by default. Is currently only supported on android. + return true; + }, + onPanResponderEnd: (evt, gestureState) => { + this.prevX = 0; + Animated.spring(this.rotation, { + friction: 4, + toValue: this.currentRotation >= 0.5 ? 1 : 0 + }).start(); + } + }); + + nextSide: (face: Face) => Face = face => { + switch (face) { + case "front": + return "back"; + case "back": + return "front"; + } + }; + + render() { + const { scale = 1 } = this.props; + const { visibleFace } = this.state; + + const rotation = this.rotation.interpolate({ + inputRange: [0, 1], + outputRange: ["0deg", "180deg"] + }); + + return ( + + + + + + ); + } +} diff --git a/storybook/stories/FlipCreditCard/FlipOnTouch.tsx b/storybook/stories/FlipCreditCard/FlipOnTouch.tsx new file mode 100644 index 0000000..1dca6b1 --- /dev/null +++ b/storybook/stories/FlipCreditCard/FlipOnTouch.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import CardView, { Props as CardViewProps } from "./CardView"; +import { Animated, TouchableOpacity } from "react-native"; +import style from "./style"; + +interface Props extends CardViewProps {} + +type Face = "front" | "back"; + +interface State { + visibleFace: Face; +} + +const animationDuration = 200; + +export default class FlipCreditCard extends React.PureComponent { + state = { visibleFace: "front" as Face }; + rotation = new Animated.Value(0); + + nextSide: (face: Face) => Face = face => { + switch (face) { + case "front": + return "back"; + case "back": + return "front"; + } + }; + + startFlipping = () => { + Animated.timing(this.rotation, { + toValue: 1, + duration: animationDuration / 2, + useNativeDriver: true + }).start(() => { + this.setState( + prevState => ({ + visibleFace: this.nextSide(prevState.visibleFace) + }), + () => this.endFlipping() + ); + }); + }; + + endFlipping = () => { + Animated.timing(this.rotation, { + toValue: 0, + duration: animationDuration / 2, + useNativeDriver: true + }).start(); + }; + + flipSides = () => { + this.startFlipping(); + }; + + render() { + const { scale = 1 } = this.props; + const { visibleFace } = this.state; + + const rotation = this.rotation.interpolate({ + inputRange: [0, 1], + outputRange: ["0deg", "90deg"] + }); + + return ( + + + + + + ); + } +} diff --git a/storybook/stories/FlipCreditCard/assets/card-chip.png b/storybook/stories/FlipCreditCard/assets/card-chip.png new file mode 100644 index 0000000..ff5dac0 Binary files /dev/null and b/storybook/stories/FlipCreditCard/assets/card-chip.png differ diff --git a/storybook/stories/FlipCreditCard/assets/gray-world-map-md.png b/storybook/stories/FlipCreditCard/assets/gray-world-map-md.png new file mode 100644 index 0000000..1e4195e Binary files /dev/null and b/storybook/stories/FlipCreditCard/assets/gray-world-map-md.png differ diff --git a/storybook/stories/FlipCreditCard/assets/visa-icon.png b/storybook/stories/FlipCreditCard/assets/visa-icon.png new file mode 100644 index 0000000..3be1120 Binary files /dev/null and b/storybook/stories/FlipCreditCard/assets/visa-icon.png differ diff --git a/storybook/stories/FlipCreditCard/common.ts b/storybook/stories/FlipCreditCard/common.ts new file mode 100644 index 0000000..71cf625 --- /dev/null +++ b/storybook/stories/FlipCreditCard/common.ts @@ -0,0 +1,64 @@ +import style from "./style"; + +export const defaultCreditCardNumberLength = 16; +export const defaultCVCPlaceholder = "---"; +export const defaultExpiryPlaceholder = "--/--"; +export const defaultCardHolderNamePlaceholder = "Card holder's name"; +export const defaultCreditCardNumberPlaceholder = "---- ---- ---- ----"; + +export function addPlaceholderStyleIfNeeded(value: any) { + if (value) { + return; + } + + return style.placeholderTextStyle; +} + +const placeholder = "---- ---- ---- ----"; +const digitsGroupLength = 4; + +function formatCreditCardString(number: string) { + return number + .split("") + .reduce( + (prev, curr, index) => + `${prev}${(index % digitsGroupLength === 0 && " ") || ""}${curr}`, + "" + ); +} + +function paddedNumber(value: string | number, length = 2) { + return `0${value}`.substr(-length); +} + +function twoDigitsDateString(date: string) { + if (!date) { + return "--"; + } + + return `${paddedNumber(date)}`.substr(0, 2); +} + +function assertValidDateFormat(expiryDate: string) { + if (expiryDate.length > 2 && expiryDate.indexOf("/") < 0) { + throw new Error("The date must have a format of mm/yy"); + } +} + +export function formatExpiryDateString(expiryDate: string) { + assertValidDateFormat(expiryDate); + + const [month, year] = expiryDate.split("/"); + const monthString = twoDigitsDateString(month); + const yearString = twoDigitsDateString(year); + + return `${monthString}/${yearString}`; +} + +export function creditCardStringWithNumber(number: string) { + const normalizedNumber = `${number.replace(/\s/g, "")}`; + + return `${normalizedNumber}${placeholder.substring( + 16 - normalizedNumber.length + )}`; +} diff --git a/storybook/stories/FlipCreditCard/style.ts b/storybook/stories/FlipCreditCard/style.ts new file mode 100644 index 0000000..fc5b049 --- /dev/null +++ b/storybook/stories/FlipCreditCard/style.ts @@ -0,0 +1,117 @@ +import { StyleSheet, Platform } from "react-native"; + +const CardSize = { width: 400, height: 253.33 }; +export const CardBackgroundColorStart = "#6833d2"; +export const CardBackgroundColorEnd = "#3f1f7e"; + +export default StyleSheet.create({ + container: { + width: "100%", + alignItems: "center", + backfaceVisibility: "visible" + }, + cardView: { + ...CardSize, + borderRadius: 15, + backgroundColor: CardBackgroundColorStart, + marginBottom: 27, + position: "relative" + }, + placeholderTextStyle: { + opacity: 0.7 + }, + cardViewFrontBackground: { + width: "100%", + height: "100%", + opacity: 0.1 + }, + cardViewFrontChip: { + position: "absolute", + width: 60, + height: 60, + top: 33, + left: 23 + }, + cardViewFrontBrandIcon: { + position: "absolute", + width: 80, + height: 53, + resizeMode: "contain", + right: 23, + top: 23 + }, + cardInformationContainer: { + position: "absolute", + bottom: 3, + flexDirection: "row", + alignItems: "center", + padding: 23 + }, + cardInformationContainerReverse: { + transform: [{ scaleX: -1 }], + opacity: 0.3 + }, + cardInformationContainerReverseText: { + color: "rgb(0, 0, 0)", + shadowRadius: 0, + fontWeight: "100" + }, + cardViewText: { + color: "rgba(255, 255, 255, 0.8)", + fontWeight: "bold", + fontFamily: Platform.select({ ios: "Courier", android: "monospace" }), + fontSize: 23, + shadowColor: "black", + textShadowColor: "black", + textShadowRadius: 1, + textShadowOffset: { width: 1, height: 1 } + }, + cardViewFrontCardNameText: { + flex: 1 + }, + cardViewFrontNumberText: { + position: "absolute", + top: 113, + left: 0, + fontSize: 30, + width: "100%", + textAlign: "center", + letterSpacing: 1.5 + }, + cardViewFrontExpiration: { + alignItems: "center", + position: "relative", + fontSize: 28 + }, + cardViewFrontExpiryLabel: { + fontSize: 16, + opacity: 0.7, + marginBottom: 5, + textShadowRadius: 0, + textShadowOffset: { width: 0, height: 0 } + }, + cardViewBackStrip: { + top: 37, + height: 48, + width: "100%", + backgroundColor: "black" + }, + cardViewBackCVCContainer: { + position: "absolute", + top: 100, + alignItems: "flex-end", + justifyContent: "center", + paddingRight: 16, + marginLeft: 16, + backgroundColor: "white", + width: "70%", + height: 29 + }, + cardViewBackCVCText: { + fontSize: 21, + fontStyle: "italic", + fontFamily: "Courier", + textAlign: "right", + fontWeight: "bold" + } +}); diff --git a/storybook/stories/LoadingList/TransitionStrategy.ts b/storybook/stories/LoadingList/TransitionStrategy.ts new file mode 100644 index 0000000..bbad8ca --- /dev/null +++ b/storybook/stories/LoadingList/TransitionStrategy.ts @@ -0,0 +1,196 @@ +import { Dimensions, Animated } from "react-native"; + +export interface TransitionStrategy { + getStyles(animatedProperty: Animated.Value): { [key: string]: any }; + configuredAnimatedValue(): Animated.Value; + targetValue(): number; + getName(): string; +} + +export const numberOfTransitions = 3; +export enum Transition { + SlideFromRight, + SlideFromBottom, + FallDown, + SlideFromLeft, + ScalingDown, + Flip +} + +export const AllTransitions = [ + Transition.SlideFromRight, + Transition.SlideFromBottom, + Transition.FallDown, + Transition.SlideFromLeft, + Transition.ScalingDown, + Transition.Flip +]; + +const screenDimensions = Dimensions.get("window"); + +class SlideFromRightTransitionStrategy implements TransitionStrategy { + getName(): string { + return "Slide from Right"; + } + + targetValue(): number { + return 0; + } + + configuredAnimatedValue(): Animated.Value { + return new Animated.Value(screenDimensions.width); + } + + getStyles(animatedProperty: Animated.Value): { [key: string]: any } { + const opacity = animatedProperty.interpolate({ + inputRange: [0, screenDimensions.width], + outputRange: [1, 0] + }); + + return { + transform: [{ translateX: animatedProperty }], + opacity + }; + } +} + +class SlideFromLeftTransitionStrategy implements TransitionStrategy { + getName(): string { + return "Slide from Left"; + } + + targetValue(): number { + return 0; + } + + configuredAnimatedValue(): Animated.Value { + return new Animated.Value(-screenDimensions.width); + } + + getStyles(animatedProperty: Animated.Value): { [key: string]: any } { + const opacity = animatedProperty.interpolate({ + inputRange: [-screenDimensions.width, 0], + outputRange: [0, 1] + }); + + return { + transform: [{ translateX: animatedProperty }], + opacity + }; + } +} + +class ScalingDownTransitionStrategy implements TransitionStrategy { + getName(): string { + return "Scaling"; + } + + targetValue(): number { + return 1; + } + + configuredAnimatedValue(): Animated.Value { + return new Animated.Value(5); + } + + getStyles(animatedProperty: Animated.Value): { [key: string]: any } { + const opacity = animatedProperty.interpolate({ + inputRange: [1, 5], + outputRange: [1, 0] + }); + + return { + transform: [{ scale: animatedProperty }], + opacity + }; + } +} + +class FlipTransitionStrategy implements TransitionStrategy { + getName(): string { + return "Flip"; + } + + targetValue(): number { + return 0; + } + + configuredAnimatedValue(): Animated.Value { + return new Animated.Value(1); + } + + getStyles(animatedProperty: Animated.Value): { [key: string]: any } { + const spin = animatedProperty.interpolate({ + inputRange: [0, 1], + outputRange: ["0deg", "270deg"] + }); + + return { + transform: [{ rotateY: spin }] + }; + } +} + +class FallDownTransitionStrategy implements TransitionStrategy { + getName(): string { + return "Fall Down"; + } + targetValue(): number { + return 1; + } + + configuredAnimatedValue(): Animated.Value { + return new Animated.Value(0); + } + + getStyles(animatedProperty: Animated.Value): { [key: string]: any } { + return { + opacity: animatedProperty + }; + } +} + +class SlideFromBottomTransitionStrategy implements TransitionStrategy { + getName(): string { + return "Slide from Bottom"; + } + + targetValue(): number { + return 0; + } + + configuredAnimatedValue(): Animated.Value { + return new Animated.Value(screenDimensions.height); + } + + getStyles(animatedProperty: Animated.Value): { [key: string]: any } { + const opacity = animatedProperty.interpolate({ + inputRange: [0, screenDimensions.width], + outputRange: [1, 0] + }); + + return { + transform: [{ translateY: animatedProperty }], + opacity + }; + } +} + +export default function getTransitionStrategy( + transition: Transition +): TransitionStrategy { + switch (transition) { + case Transition.FallDown: + return new FallDownTransitionStrategy(); + case Transition.SlideFromBottom: + return new SlideFromBottomTransitionStrategy(); + case Transition.SlideFromRight: + return new SlideFromRightTransitionStrategy(); + case Transition.SlideFromLeft: + return new SlideFromLeftTransitionStrategy(); + case Transition.ScalingDown: + return new ScalingDownTransitionStrategy(); + case Transition.Flip: + return new FlipTransitionStrategy(); + } +} diff --git a/storybook/stories/LoadingList/index.tsx b/storybook/stories/LoadingList/index.tsx new file mode 100644 index 0000000..5b25cfb --- /dev/null +++ b/storybook/stories/LoadingList/index.tsx @@ -0,0 +1,235 @@ +import React from "react"; +import { Animated, FlatList, View, TouchableOpacity, Text } from "react-native"; +import getTransitionStrategy, { + Transition, + TransitionStrategy, + AllTransitions +} from "./TransitionStrategy"; +import styles from "./styles"; + +/** + * Genearates a random string. + * + * The generated string has high probabilites of being unique, + * even though uniqueness is not ensured. + * + * It is used to generate a random key so a flat list + * can differentiate between Views and work its recycling magic. + * + * @returns {string} An _unique_ random key + */ +function generateRandomKey(): string { + return Math.random() + .toString(36) + .substring(7); +} + +interface LoadingListProps {} +interface LoadingListState { + items: ItemData[]; + selectedTransition: Transition; +} + +interface ItemData { + key: string; +} + +const defaultTransition = Transition.SlideFromRight; +const animationDelay = 150; + +/** + * Returns an array of elements + * + * It is used as a building block for objects. This function + * is very usueful when using in conjunction with `map`. + * + * @example + * sequence(10).map(() => ) + * + * @param {number} numberOfElements The number of elements contained in the array. + * @returns {string[]} An array of strings. + */ +function sequence(numberOfElements: number): string[] { + return "a".repeat(numberOfElements).split(""); +} + +/** + * React component that renders a list of items, with a transition effect. + * + * @export + * @class LoadingList + * @extends {React.PureComponent} + */ +export default class LoadingList extends React.PureComponent< + LoadingListProps, + LoadingListState +> { + state = { items: [], selectedTransition: defaultTransition }; + initialNumberOfItems = 10; + + constructor(props: LoadingListProps) { + super(props); + + this.transitionStrategy = getTransitionStrategy(defaultTransition); + this.resetAnimation(); + + this.state = { + items: sequence(this.initialNumberOfItems).map(() => ({ + key: generateRandomKey() + })), + selectedTransition: defaultTransition + }; + } + + animatedProperty: Animated.Value[]; + transitionStrategy: TransitionStrategy; + + componentDidMount() { + this.resetAnimation(); + } + + /** + * Run the current animation. + * + * @memberof LoadingList + */ + reload = () => { + this.resetAnimation(); + this.forceUpdate(); + }; + + /** + * Configures the animation to be run. + * + * @memberof LoadingList + */ + resetAnimation = () => { + this.resetAnimatedValues(); + this.configureAnimation(); + }; + + /** + * As part of the animation configuration, the animated values are set to their initial values. + * + * @memberof LoadingList + */ + resetAnimatedValues = () => { + this.animatedProperty = sequence(this.initialNumberOfItems).map(() => + this.transitionStrategy.configuredAnimatedValue() + ); + }; + + /** + * Configures an starts the animation. + * + * @memberof LoadingList + */ + configureAnimation = () => { + Animated.stagger( + animationDelay, + this.animatedProperty.map(value => + Animated.spring(value, { + toValue: this.transitionStrategy.targetValue(), + useNativeDriver: true + }) + ) + ).start(); + }; + + /** + * Forces the FlatList to be re-initialized. + * + * This method is needed becuase of the view recycling mechanism + * of the FlatList. + * + * @memberof LoadingList + */ + forceUpdate() { + const { items } = this.state; + + this.setState(() => ({ items: [] })); + + setTimeout(() => { + this.setState({ items }); + }); + } + + /** + * Selects the next transition on the list. + * + * @param {Transition} currentTransition + * @returns + * @memberof LoadingList + */ + getNextTransition(currentTransition: Transition) { + const currentIndex = AllTransitions.findIndex( + value => value === currentTransition + ); + + const nextIndex = (currentIndex + 1) % AllTransitions.length; + return AllTransitions[nextIndex]; + } + + showNextTransition = () => { + const { selectedTransition } = this.state; + + const nextTransition = this.getNextTransition(selectedTransition); + this.transitionStrategy = getTransitionStrategy(nextTransition); + + this.setState( + { + selectedTransition: nextTransition + }, + () => this.reload() + ); + }; + + keyExtractor(item: ItemData) { + return item.key; + } + + renderItem = ({ item, index }: { item: ItemData; index: number }) => { + if (index < this.initialNumberOfItems) { + return ( + + ); + } + + return ; + }; + + render() { + const { items } = this.state; + + return ( + + + + + {this.transitionStrategy.getName()} + + + + + RELOAD + + + + + ); + } +} diff --git a/storybook/stories/LoadingList/styles.ts b/storybook/stories/LoadingList/styles.ts new file mode 100644 index 0000000..ec80ede --- /dev/null +++ b/storybook/stories/LoadingList/styles.ts @@ -0,0 +1,59 @@ +import { StyleSheet } from "react-native"; + +export default StyleSheet.create({ + box: { + backgroundColor: "white", + height: 70, + marginBottom: 7 + }, + container: { + backgroundColor: "rgb(207, 215, 219)", + }, + header: { + backgroundColor: "rgb(93, 122, 137)", + flexDirection: "row", + padding: 20, + marginBottom: 10, + alignItems: "center", + shadowColor: "black", + shadowOffset: { width: 0, height: 3 }, + shadowRadius: 5, + shadowOpacity: 0.5 + }, + layoutPicker: { + flexDirection: "row", + alignItems: "center", + flex: 1, + marginRight: 20, + padding: 20 + }, + layoutPickerText: { + color: "#ccc", + fontSize: 18, + fontWeight: "bold", + flex: 1 + }, + reloadButton: { + backgroundColor: "rgb(255, 188, 0)", + paddingHorizontal: 20, + paddingVertical: 15 + }, + reloadButtonText: { + color: "white", + fontWeight: "bold", + fontSize: 15 + }, + itemsContainer: { + paddingHorizontal: 10 + }, + dropDownArrow: { + width: 0, + height: 0, + borderLeftWidth: 6, + borderLeftColor: "transparent", + borderRightWidth: 6, + borderRightColor: "transparent", + borderTopWidth: 6, + borderTopColor: "#ccc" + } +}); diff --git a/storybook/stories/index.tsx b/storybook/stories/index.tsx index f9d41ac..26c0fc6 100644 --- a/storybook/stories/index.tsx +++ b/storybook/stories/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { View, Dimensions, Text } from "react-native"; +import { View, Dimensions, Text, SafeAreaView } from "react-native"; import { storiesOf } from "@storybook/react-native"; import TopToBottomRowAnimation from "./TopToBottomRowAnimation"; import LeftToRightRowAnimation from "./LeftToRightRowAnimation"; @@ -9,14 +9,13 @@ import AnimatedBar from "./AnimatedBar"; import { seq } from "../../components/utils"; import GrowAndDisappear from "./GrowAndDisappear"; import Banner from "./Banner"; +import GestureCardView from './FlipCreditCard/FlipOnGesture'; +import FlipOnTapCardView from './FlipCreditCard/FlipOnTouch'; +import LoadingList from './LoadingList'; -storiesOf("Row Animation", module). - add("Appear from the left", () => ( - - )) - .add("Appear from the top", () => ( - - )); +storiesOf("Row Animation", module) + .add("Appear from the left", () => ) + .add("Appear from the top", () => ); const dimensions = Dimensions.get("window"); @@ -50,4 +49,29 @@ storiesOf("Simple animations", module) Some text + )) + .add("Flipping credit card (with gestures)", () => ( + + + + )).add("Flipping credit card (flip on tap)", () => ( + + + + )).add("Loading list transitions", () => ( + ));