Add core functionality for mobile post scheduling app
Adds necessary dependencies including axios and react-native-calendars to pnpm-lock.yaml. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7b0991ce-c7b8-4c82-9acc-fd3f9e762a01 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: dc1266fa-8375-43e1-aca0-9df31350f647 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/86064bd6-c937-4ca5-a5bf-bbef5749fb60/7b0991ce-c7b8-4c82-9acc-fd3f9e762a01/kWnlAIM Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Image } from "expo-image";
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
import { PostizIntegration } from "@/context/PostizContext";
|
||||
|
||||
interface ChannelChipProps {
|
||||
integration: PostizIntegration;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export function ChannelChip({ integration, selected, onToggle }: ChannelChipProps) {
|
||||
const colors = useColors();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onToggle}
|
||||
activeOpacity={0.7}
|
||||
style={[
|
||||
styles.chip,
|
||||
{
|
||||
backgroundColor: selected ? colors.primary + "20" : colors.card,
|
||||
borderColor: selected ? colors.primary : colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{integration.picture ? (
|
||||
<Image
|
||||
source={{ uri: integration.picture }}
|
||||
style={styles.avatar}
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.avatarFallback, { backgroundColor: colors.secondary }]}>
|
||||
<Feather name="globe" size={12} color={colors.mutedForeground} />
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
styles.name,
|
||||
{ color: selected ? colors.primary : colors.foreground },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{integration.name}
|
||||
</Text>
|
||||
{selected && (
|
||||
<Feather name="check" size={12} color={colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chip: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
gap: 6,
|
||||
maxWidth: 150,
|
||||
},
|
||||
avatar: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
},
|
||||
avatarFallback: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
name: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter_500Medium",
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { Component, ComponentType, PropsWithChildren } from "react";
|
||||
|
||||
import { ErrorFallback, ErrorFallbackProps } from "@/components/ErrorFallback";
|
||||
|
||||
export type ErrorBoundaryProps = PropsWithChildren<{
|
||||
FallbackComponent?: ComponentType<ErrorFallbackProps>;
|
||||
onError?: (error: Error, stackTrace: string) => void;
|
||||
}>;
|
||||
|
||||
type ErrorBoundaryState = { error: Error | null };
|
||||
|
||||
/**
|
||||
* This is a special case for for using the class components. Error boundaries must be class components because React only provides error boundary functionality through lifecycle methods (componentDidCatch and getDerivedStateFromError) which are not available in functional components.
|
||||
* https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
||||
*/
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = { error: null };
|
||||
|
||||
static defaultProps: {
|
||||
FallbackComponent: ComponentType<ErrorFallbackProps>;
|
||||
} = {
|
||||
FallbackComponent: ErrorFallback,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: { componentStack: string }): void {
|
||||
if (typeof this.props.onError === "function") {
|
||||
this.props.onError(error, info.componentStack);
|
||||
}
|
||||
}
|
||||
|
||||
resetError = (): void => {
|
||||
this.setState({ error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { FallbackComponent } = this.props;
|
||||
|
||||
return this.state.error && FallbackComponent ? (
|
||||
<FallbackComponent
|
||||
error={this.state.error}
|
||||
resetError={this.resetError}
|
||||
/>
|
||||
) : (
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { reloadAppAsync } from "expo";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
|
||||
export type ErrorFallbackProps = {
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
};
|
||||
|
||||
export function ErrorFallback({ error, resetError }: ErrorFallbackProps) {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
const handleRestart = async () => {
|
||||
try {
|
||||
await reloadAppAsync();
|
||||
} catch (restartError) {
|
||||
console.error("Failed to restart app:", restartError);
|
||||
resetError();
|
||||
}
|
||||
};
|
||||
|
||||
const formatErrorDetails = (): string => {
|
||||
let details = `Error: ${error.message}\n\n`;
|
||||
if (error.stack) {
|
||||
details += `Stack Trace:\n${error.stack}`;
|
||||
}
|
||||
return details;
|
||||
};
|
||||
|
||||
const monoFont = Platform.select({
|
||||
ios: "Menlo",
|
||||
android: "monospace",
|
||||
default: "monospace",
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
{__DEV__ ? (
|
||||
<Pressable
|
||||
onPress={() => setIsModalVisible(true)}
|
||||
accessibilityLabel="View error details"
|
||||
accessibilityRole="button"
|
||||
style={({ pressed }) => [
|
||||
styles.topButton,
|
||||
{
|
||||
top: insets.top + 16,
|
||||
backgroundColor: colors.card,
|
||||
opacity: pressed ? 0.8 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Feather name="alert-circle" size={20} color={colors.foreground} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<View style={styles.content}>
|
||||
<Text style={[styles.title, { color: colors.foreground }]}>
|
||||
Something went wrong
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.message, { color: colors.mutedForeground }]}>
|
||||
Please reload the app to continue.
|
||||
</Text>
|
||||
|
||||
<Pressable
|
||||
onPress={handleRestart}
|
||||
style={({ pressed }) => [
|
||||
styles.button,
|
||||
{
|
||||
backgroundColor: colors.primary,
|
||||
opacity: pressed ? 0.9 : 1,
|
||||
transform: [{ scale: pressed ? 0.98 : 1 }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.buttonText,
|
||||
{ color: colors.primaryForeground },
|
||||
]}
|
||||
>
|
||||
Try Again
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{__DEV__ ? (
|
||||
<Modal
|
||||
visible={isModalVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => setIsModalVisible(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View
|
||||
style={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: colors.background },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.modalHeader,
|
||||
{ borderBottomColor: colors.border },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: colors.foreground }]}>
|
||||
Error Details
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => setIsModalVisible(false)}
|
||||
accessibilityLabel="Close error details"
|
||||
accessibilityRole="button"
|
||||
style={({ pressed }) => [
|
||||
styles.closeButton,
|
||||
{ opacity: pressed ? 0.6 : 1 },
|
||||
]}
|
||||
>
|
||||
<Feather name="x" size={24} color={colors.foreground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.modalScrollView}
|
||||
contentContainerStyle={[
|
||||
styles.modalScrollContent,
|
||||
{ paddingBottom: insets.bottom + 16 },
|
||||
]}
|
||||
showsVerticalScrollIndicator
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.errorContainer,
|
||||
{ backgroundColor: colors.card },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.errorText,
|
||||
{
|
||||
color: colors.foreground,
|
||||
fontFamily: monoFont,
|
||||
},
|
||||
]}
|
||||
selectable
|
||||
>
|
||||
{formatErrorDetails()}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 24,
|
||||
},
|
||||
content: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 16,
|
||||
width: "100%",
|
||||
maxWidth: 600,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
lineHeight: 40,
|
||||
},
|
||||
message: {
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
},
|
||||
topButton: {
|
||||
position: "absolute",
|
||||
right: 16,
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 10,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 24,
|
||||
minWidth: 200,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
buttonText: {
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
fontSize: 16,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
modalContainer: {
|
||||
width: "100%",
|
||||
height: "90%",
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "600",
|
||||
},
|
||||
closeButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
modalScrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
modalScrollContent: {
|
||||
padding: 16,
|
||||
},
|
||||
errorContainer: {
|
||||
width: "100%",
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
padding: 16,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
width: "100%",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
KeyboardAwareScrollView,
|
||||
KeyboardAwareScrollViewProps,
|
||||
} from "react-native-keyboard-controller";
|
||||
import { Platform, ScrollView, ScrollViewProps } from "react-native";
|
||||
|
||||
type Props = KeyboardAwareScrollViewProps & ScrollViewProps;
|
||||
|
||||
export function KeyboardAwareScrollViewCompat({
|
||||
children,
|
||||
keyboardShouldPersistTaps = "handled",
|
||||
...props
|
||||
}: Props) {
|
||||
if (Platform.OS === "web") {
|
||||
return (
|
||||
<ScrollView keyboardShouldPersistTaps={keyboardShouldPersistTaps} {...props}>
|
||||
{children}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</KeyboardAwareScrollView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Swipeable } from "react-native-gesture-handler";
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
import { PostizPost } from "@/context/PostizContext";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
|
||||
interface PostCardProps {
|
||||
post: PostizPost;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
try {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
function getNetworkIcon(type?: string): React.ComponentProps<typeof Feather>["name"] {
|
||||
const t = (type ?? "").toLowerCase();
|
||||
if (t.includes("twitter") || t.includes("x")) return "twitter";
|
||||
if (t.includes("linkedin")) return "linkedin";
|
||||
if (t.includes("instagram")) return "instagram";
|
||||
if (t.includes("facebook")) return "facebook";
|
||||
if (t.includes("youtube")) return "youtube";
|
||||
return "globe";
|
||||
}
|
||||
|
||||
export function PostCard({ post, onDelete }: PostCardProps) {
|
||||
const colors = useColors();
|
||||
const swipeRef = useRef<Swipeable>(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
Alert.alert("Delete Post", "Are you sure you want to delete this post?", [
|
||||
{
|
||||
text: "Cancel",
|
||||
style: "cancel",
|
||||
onPress: () => swipeRef.current?.close(),
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await onDelete(post.id);
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const renderRightActions = (
|
||||
_progress: Animated.AnimatedInterpolation<number>,
|
||||
dragX: Animated.AnimatedInterpolation<number>
|
||||
) => {
|
||||
const scale = dragX.interpolate({
|
||||
inputRange: [-80, 0],
|
||||
outputRange: [1, 0.8],
|
||||
extrapolate: "clamp",
|
||||
});
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.deleteAction, { backgroundColor: colors.destructive }]}
|
||||
onPress={handleDelete}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Animated.View style={{ transform: [{ scale }] }}>
|
||||
<Feather name="trash-2" size={20} color="#fff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const integrations = post.integrations ?? (post.integration ? [post.integration] : []);
|
||||
const truncatedContent =
|
||||
post.content.length > 140
|
||||
? post.content.slice(0, 140) + "…"
|
||||
: post.content;
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
ref={swipeRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
friction={2}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{ backgroundColor: colors.card, borderBottomColor: colors.border },
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.integrations}>
|
||||
{integrations.slice(0, 3).map((intg) => (
|
||||
<View
|
||||
key={intg.id}
|
||||
style={[
|
||||
styles.networkIcon,
|
||||
{ backgroundColor: colors.secondary },
|
||||
]}
|
||||
>
|
||||
<Feather
|
||||
name={getNetworkIcon(intg.type ?? intg.internalType)}
|
||||
size={12}
|
||||
color={colors.mutedForeground}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{integrations.length > 3 && (
|
||||
<Text style={[styles.moreText, { color: colors.mutedForeground }]}>
|
||||
+{integrations.length - 3}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<StatusBadge status={post.status} />
|
||||
</View>
|
||||
<Text style={[styles.content, { color: colors.foreground }]}>
|
||||
{truncatedContent}
|
||||
</Text>
|
||||
<View style={styles.footer}>
|
||||
<Feather name="clock" size={12} color={colors.mutedForeground} />
|
||||
<Text style={[styles.date, { color: colors.mutedForeground }]}>
|
||||
{formatDate(post.publishDate)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
gap: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
integrations: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
networkIcon: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
moreText: {
|
||||
fontSize: 11,
|
||||
fontFamily: "Inter_500Medium",
|
||||
},
|
||||
content: {
|
||||
fontSize: 14,
|
||||
fontFamily: "Inter_400Regular",
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
date: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter_400Regular",
|
||||
},
|
||||
deleteAction: {
|
||||
width: 72,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
|
||||
type PostStatus = "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: PostStatus;
|
||||
}
|
||||
|
||||
export function StatusBadge({ status }: StatusBadgeProps) {
|
||||
const colors = useColors();
|
||||
|
||||
const config: Record<PostStatus, { bg: string; text: string; label: string }> = {
|
||||
QUEUE: { bg: colors.warning + "25", text: colors.warning, label: "Queue" },
|
||||
PUBLISHED: { bg: colors.success + "25", text: colors.success, label: "Published" },
|
||||
ERROR: { bg: colors.error + "25", text: colors.error, label: "Error" },
|
||||
DRAFT: { bg: colors.muted, text: colors.mutedForeground, label: "Draft" },
|
||||
};
|
||||
|
||||
const { bg, text, label } = config[status] ?? config.DRAFT;
|
||||
|
||||
return (
|
||||
<View style={[styles.badge, { backgroundColor: bg }]}>
|
||||
<View style={[styles.dot, { backgroundColor: text }]} />
|
||||
<Text style={[styles.label, { color: text }]}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 20,
|
||||
gap: 4,
|
||||
},
|
||||
dot: {
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: 3,
|
||||
},
|
||||
label: {
|
||||
fontSize: 11,
|
||||
fontFamily: "Inter_600SemiBold",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user