Skip to content

calculator  #1

Description

@zeravez

import React, { useCallback, useReducer, useRef, useState, useEffect } from "react";
import {
Dimensions,
Platform,
Pressable,
StatusBar,
StyleSheet,
Text,
View,
useColorScheme,
} from "react-native";

import * as Clipboard from "expo-clipboard";
import Animated, {
useAnimatedStyle,
useSharedValue,
withSequence,
withTiming,
} from "react-native-reanimated";

import * as Haptics from "expo-haptics";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useColors } from "@/hooks/useColors";

const { width } = Dimensions.get("window");
const BUTTON_SIZE = Math.floor((width - 64) / 4);

type CalcAction =
| { type: "DIGIT"; value: string }
| { type: "OPERATOR"; value: string }
| { type: "EQUALS" }
| { type: "CLEAR" }
| { type: "TOGGLE_SIGN" }
| { type: "PERCENT" }
| { type: "DECIMAL" }
| { type: "BACKSPACE" };

type CalcState = {
display: string;
expression: string;
operand: string | null;
operator: string | null;
waiting: boolean;
hasResult: boolean;
};

const initialState: CalcState = {
display: "0",
expression: "",
operand: null,
operator: null,
waiting: false,
hasResult: false,
};

function calculate(a: number, b: number, op: string): number {
switch (op) {
case "+":
return a + b;
case "−":
return a - b;
case "×":
return a * b;
case "÷":
return b === 0 ? NaN : a / b;
default:
return b;
}
}

function safeDisplay(value: string) {
if (value === "Error") return value;

const num = Number(value);
if (Number.isNaN(num)) return "Error";
if (!Number.isFinite(num)) return "Error";

if (Math.abs(num) > 1e15) return num.toExponential(6);

return value;
}

function reducer(state: CalcState, action: CalcAction): CalcState {
switch (action.type) {
case "DIGIT": {
if (state.hasResult) {
return { ...initialState, display: action.value };
}

  if (state.waiting) {
    return { ...state, display: action.value, waiting: false };
  }

  if (state.display === "0") {
    return { ...state, display: action.value };
  }

  return { ...state, display: state.display + action.value };
}

case "DECIMAL": {
  if (state.waiting || state.hasResult) {
    return { ...state, display: "0", waiting: false, hasResult: false };
  }

  if (state.display.includes(".")) return state;

  return { ...state, display: state.display + "." };
}

case "OPERATOR": {
  const current = Number(state.display);

  if (state.operator && state.operand !== null && !state.waiting) {
    const result = calculate(
      Number(state.operand),
      current,
      state.operator
    );

    const resultStr = String(result);

    return {
      ...state,
      display: safeDisplay(resultStr),
      operand: resultStr,
      operator: action.value,
      expression: `${resultStr} ${action.value}`,
      waiting: true,
    };
  }

  return {
    ...state,
    operand: state.display,
    operator: action.value,
    expression: `${state.display} ${action.value}`,
    waiting: true,
  };
}

case "EQUALS": {
  if (!state.operator || state.operand === null) return state;

  const result = calculate(
    Number(state.operand),
    Number(state.display),
    state.operator
  );

  const resultStr = String(result);

  return {
    ...state,
    display: safeDisplay(resultStr),
    expression: `${state.operand} ${state.operator} ${state.display} =`,
    operator: null,
    operand: null,
    hasResult: true,
  };
}

case "CLEAR":
  return initialState;

case "BACKSPACE": {
  if (state.waiting || state.hasResult) return state;

  const next = state.display.slice(0, -1);

  return {
    ...state,
    display: next.length ? next : "0",
  };
}

case "TOGGLE_SIGN": {
  if (state.display === "0" || state.display === "Error") return state;

  return {
    ...state,
    display: state.display.startsWith("-")
      ? state.display.slice(1)
      : "-" + state.display,
  };
}

case "PERCENT": {
  const value = Number(state.display);
  if (Number.isNaN(value)) return state;

  return {
    ...state,
    display: String(value / 100),
  };
}

default:
  return state;

}
}

type ButtonKind = "number" | "operator" | "equal" | "func";

function CalcButton({
label,
kind,
wide,
onPress,
}: {
label: string;
kind: ButtonKind;
wide?: boolean;
onPress: () => void;
}) {
const colors = useColors();
const scale = useSharedValue(1);

const anim = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));

const press = () => {
scale.value = withSequence(
withTiming(0.88, { duration: 80 }),
withTiming(1, { duration: 100 })
);

if (Platform.OS !== "web") {
  Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}

onPress();

};

const bg =
kind === "number"
? colors.numberBtn
: kind === "operator"
? colors.operatorBtn
: kind === "equal"
? colors.equalBtn
: colors.funcBtn;

const color =
kind === "number"
? colors.numberText
: kind === "operator"
? colors.operatorText
: kind === "equal"
? colors.equalText
: colors.funcText;

return (
<Pressable onPress={press} style={{ margin: 4 }}>
<Animated.View
style={[
styles.button,
anim,
{
width: wide ? BUTTON_SIZE * 2 + 8 : BUTTON_SIZE,
height: BUTTON_SIZE,
backgroundColor: bg,
borderRadius: BUTTON_SIZE / 2,
},
]}
>
<Text style={[styles.text, { color }]}>{label}
</Animated.View>

);
}

export default function CalculatorScreen() {
const [state, dispatch] = useReducer(reducer, initialState);
const colors = useColors();
const insets = useSafeAreaInsets();
const isDark = useColorScheme() === "dark";

const scale = useSharedValue(1);

const displayAnim = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));

const animate = () => {
scale.value = withSequence(
withTiming(0.95, { duration: 60 }),
withTiming(1, { duration: 100 })
);
};

const dispatchAction = useCallback((action: CalcAction) => {
if (action.type === "OPERATOR" || action.type === "EQUALS") {
animate();
}
dispatch(action);
}, []);

const [copied, setCopied] = useState(false);
const timer = useRef<ReturnType | null>(null);

const copy = useCallback(async () => {
if (state.display === "Error") return;

await Clipboard.setStringAsync(state.display);

if (Platform.OS !== "web") {
  Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}

setCopied(true);

if (timer.current) clearTimeout(timer.current);

timer.current = setTimeout(() => setCopied(false), 1200);

}, [state.display]);

useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);

const value = safeDisplay(state.display);

const fontSize =
value.length > 12 ? 36 : value.length > 8 ? 48 : 64;

const top = insets.top + 20;
const bottom = insets.bottom + 16;

return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<StatusBar barStyle={isDark ? "light-content" : "dark-content"} />

  <Pressable
    onPress={copy}
    style={[
      styles.display,
      { backgroundColor: colors.displayBg, paddingTop: top },
    ]}
  >
    <Text style={[styles.expr, { color: colors.mutedForeground }]}>
      {state.expression}
    </Text>

    <Animated.Text
      style={[
        styles.value,
        displayAnim,
        { color: colors.displayText, fontSize },
      ]}
    >
      {value}
    </Animated.Text>

    {copied && (
      <View style={[styles.badge, { backgroundColor: colors.accent }]}>
        <Text style={{ color: colors.accentForeground }}>Copied</Text>
      </View>
    )}
  </Pressable>

  <View style={[styles.pad, { paddingBottom: bottom }]}>
    <View style={styles.row}>
      <CalcButton label="C" kind="func" onPress={() => dispatchAction({ type: "CLEAR" })} />
      <CalcButton label="+/-" kind="func" onPress={() => dispatchAction({ type: "TOGGLE_SIGN" })} />
      <CalcButton label="%" kind="func" onPress={() => dispatchAction({ type: "PERCENT" })} />
      <CalcButton label="÷" kind="operator" onPress={() => dispatchAction({ type: "OPERATOR", value: "÷" })} />
    </View>

    <View style={styles.row}>
      <CalcButton label="7" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "7" })} />
      <CalcButton label="8" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "8" })} />
      <CalcButton label="9" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "9" })} />
      <CalcButton label="×" kind="operator" onPress={() => dispatchAction({ type: "OPERATOR", value: "×" })} />
    </View>

    <View style={styles.row}>
      <CalcButton label="4" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "4" })} />
      <CalcButton label="5" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "5" })} />
      <CalcButton label="6" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "6" })} />
      <CalcButton label="−" kind="operator" onPress={() => dispatchAction({ type: "OPERATOR", value: "−" })} />
    </View>

    <View style={styles.row}>
      <CalcButton label="1" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "1" })} />
      <CalcButton label="2" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "2" })} />
      <CalcButton label="3" kind="number" onPress={() => dispatchAction({ type: "DIGIT", value: "3" })} />
      <CalcButton label="+" kind="operator" onPress={() => dispatchAction({ type: "OPERATOR", value: "+" })} />
    </View>

    <View style={styles.row}>
      <CalcButton
        label="0"
        kind="number"
        wide
        onPress={() => dispatchAction({ type: "DIGIT", value: "0" })}
      />
      <CalcButton label="." kind="number" onPress={() => dispatchAction({ type: "DECIMAL" })} />
      <CalcButton label="=" kind="equal" onPress={() => dispatchAction({ type: "EQUALS" })} />
    </View>
  </View>
</View>

);
}

const styles = StyleSheet.create({
container: { flex: 1 },
display: {
flex: 1.5,
justifyContent: "flex-end",
alignItems: "flex-end",
paddingHorizontal: 24,
},
expr: { fontSize: 16, marginBottom: 6 },
value: { letterSpacing: -1, fontWeight: "300" },
pad: { paddingHorizontal: 20, paddingTop: 10 },
row: { flexDirection: "row", justifyContent: "center" },
button: { justifyContent: "center", alignItems: "center" },
text: { fontSize: 26, fontWeight: "600" },
badge: {
position: "absolute",
bottom: 10,
right: 12,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 20,
},
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions