Big improvements on the app
This commit is contained in:
parent
5edc860019
commit
1ab7c2a180
11 changed files with 1826 additions and 66 deletions
|
|
@ -1,46 +1,112 @@
|
|||
import { Tabs } from "expo-router";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import { HapticTab } from "@/components/HapticTab";
|
||||
import TabBarBackground from "@/components/ui/TabBarBackground";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
import { MessageCircle, Calendar, User } from "lucide-react-native";
|
||||
import {
|
||||
MessageCircle,
|
||||
Calendar,
|
||||
List,
|
||||
Home,
|
||||
Grid3X3,
|
||||
} from "lucide-react-native";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
|
||||
// Colors and theming
|
||||
tabBarActiveTintColor: "#3b82f6", // Modern blue color
|
||||
tabBarInactiveTintColor: isDark ? "#9CA3AF" : "#6B7280",
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
tabBarBackground: TabBarBackground,
|
||||
|
||||
// Enhanced styling
|
||||
tabBarStyle: {
|
||||
backgroundColor: isDark ? "#1F2937" : "#FFFFFF",
|
||||
borderTopWidth: 0,
|
||||
elevation: 20,
|
||||
shadowColor: "#000000",
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 20,
|
||||
height: Platform.OS === "ios" ? 88 : 70,
|
||||
paddingTop: 8,
|
||||
paddingBottom: Platform.OS === "ios" ? 16 : 8,
|
||||
paddingHorizontal: 12,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
// Label styling
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
marginTop: 4,
|
||||
marginBottom: Platform.OS === "ios" ? 0 : 4,
|
||||
},
|
||||
|
||||
// Icon styling
|
||||
tabBarIconStyle: {
|
||||
marginTop: 2,
|
||||
},
|
||||
|
||||
// Animation and interaction
|
||||
tabBarHideOnKeyboard: true,
|
||||
tabBarAllowFontScaling: false,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Discussions",
|
||||
tabBarIcon: ({ size, color }) => (
|
||||
<MessageCircle size={size} color={color} />
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<MessageCircle
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Discussions",
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="planning"
|
||||
options={{
|
||||
title: "Planning",
|
||||
tabBarIcon: ({ size, color }) => (
|
||||
<Calendar size={size} color={color} />
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<Calendar
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Planning",
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
name="tablos"
|
||||
options={{
|
||||
title: "Profile",
|
||||
tabBarIcon: ({ size, color }) => <User size={size} color={color} />,
|
||||
title: "Tablos",
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<Grid3X3
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Tablos",
|
||||
// Optional: Add a badge for notifications
|
||||
tabBarBadge: undefined, // You can set this to a number for notifications
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,384 @@
|
|||
import { router } from "expo-router";
|
||||
import { ChannelList } from "stream-chat-expo";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
} from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Search } from "lucide-react-native";
|
||||
import React from "react";
|
||||
import { useTablosList } from "@/hooks/tablos";
|
||||
import { ColorMap } from "@/constants/colors";
|
||||
import { UserTablo } from "@/types/tablos.types";
|
||||
|
||||
// Custom Avatar Component for Channel List
|
||||
const CustomChannelAvatar = ({
|
||||
channel,
|
||||
tablos,
|
||||
}: {
|
||||
channel: any;
|
||||
tablos: UserTablo[];
|
||||
}) => {
|
||||
const tabloId = channel?.id || "";
|
||||
const tablo = tablos?.find((t) => t.id === tabloId);
|
||||
const tabloColor = tablo?.color || "bg-blue-500";
|
||||
const tabloName = tablo?.name || channel?.data?.name || "Tablo";
|
||||
|
||||
// Get members info
|
||||
const members = channel?.state?.members || {};
|
||||
const memberCount = Object.keys(members).length;
|
||||
|
||||
// Generate initials from tablo name
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
// // Create gradient colors based on tablo color
|
||||
const getTabloGradientColors = (colorKey: string): [string, string] => {
|
||||
const baseColor = ColorMap[colorKey] || ColorMap["bg-blue-500"];
|
||||
|
||||
// Create a lighter version for gradient effect
|
||||
const lightenColor = (hex: string, percent: number): string => {
|
||||
const num = parseInt(hex.replace("#", ""), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = Math.min(255, Math.max(0, (num >> 16) + amt));
|
||||
const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
|
||||
const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt));
|
||||
return "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1);
|
||||
};
|
||||
|
||||
// Create a darker version for gradient effect
|
||||
const darkenColor = (hex: string, percent: number): string => {
|
||||
const num = parseInt(hex.replace("#", ""), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = Math.min(255, Math.max(0, (num >> 16) - amt));
|
||||
const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) - amt));
|
||||
const B = Math.min(255, Math.max(0, (num & 0x0000ff) - amt));
|
||||
return "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1);
|
||||
};
|
||||
|
||||
const lightColor = lightenColor(baseColor, 15);
|
||||
const darkColor = darkenColor(baseColor, 10);
|
||||
|
||||
return [lightColor, darkColor];
|
||||
};
|
||||
|
||||
const initials = getInitials(tabloName);
|
||||
const gradientColors = getTabloGradientColors(tabloColor);
|
||||
|
||||
return (
|
||||
<View style={styles.avatarContainer}>
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.avatarGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Text style={styles.avatarInitials}>{initials}</Text>
|
||||
|
||||
{/* Member count indicator for group channels */}
|
||||
{memberCount > 2 && (
|
||||
<View style={styles.memberCountBadge}>
|
||||
<Text style={styles.memberCountText}>{memberCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Decorative ring */}
|
||||
<View
|
||||
style={[
|
||||
styles.avatarRing,
|
||||
{ borderColor: `${ColorMap[tabloColor]}30` },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Status indicator (online/active) */}
|
||||
<View style={styles.statusIndicator} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function HomeScreen() {
|
||||
const user = useUser();
|
||||
const { data: tablos } = useTablosList();
|
||||
const filters = { members: { $in: [user.id] }, type: "messaging" };
|
||||
|
||||
// Create a wrapper component for the avatar that has access to tablos data
|
||||
const AvatarWithTablos = ({ channel }: { channel: any }) => (
|
||||
<CustomChannelAvatar channel={channel} tablos={tablos || []} />
|
||||
);
|
||||
|
||||
return (
|
||||
<ChannelList
|
||||
filters={filters}
|
||||
onSelect={(channel) => {
|
||||
router.push(`/channel/${channel.cid}`);
|
||||
}}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
||||
{/* Beautiful Header */}
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerBottom}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.headerTitle}>Discussions</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Gérez les conversations de vos tablos
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.searchButton}>
|
||||
<Search size={20} color="#3b82f6" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Decorative Elements
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} /> */}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Channel List */}
|
||||
<View style={styles.channelListContainer}>
|
||||
<ChannelList
|
||||
filters={filters}
|
||||
onSelect={(channel) => {
|
||||
router.push(`/channel/${channel.cid}`);
|
||||
}}
|
||||
PreviewAvatar={AvatarWithTablos}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
headerGradient: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 25,
|
||||
paddingHorizontal: 20,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
},
|
||||
headerContent: {
|
||||
zIndex: 10,
|
||||
},
|
||||
headerTop: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
userInfo: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
avatar: {
|
||||
marginRight: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
borderWidth: 3,
|
||||
borderColor: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
greetingContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
greeting: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255, 255, 255, 0.9)",
|
||||
fontWeight: "500",
|
||||
},
|
||||
userName: {
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
marginTop: 2,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
},
|
||||
actionButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
},
|
||||
notificationBadge: {
|
||||
position: "absolute",
|
||||
top: -2,
|
||||
right: -2,
|
||||
backgroundColor: "#ef4444",
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 2,
|
||||
borderColor: "white",
|
||||
},
|
||||
badgeText: {
|
||||
color: "white",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
headerBottom: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 28,
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
marginBottom: 4,
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
fontWeight: "400",
|
||||
},
|
||||
searchButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: "white",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: "absolute",
|
||||
top: -50,
|
||||
right: -30,
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: "absolute",
|
||||
bottom: -20,
|
||||
left: -20,
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
channelListContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
marginTop: -10,
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
paddingTop: 10,
|
||||
},
|
||||
|
||||
// Custom Avatar Styles
|
||||
avatarContainer: {
|
||||
position: "relative",
|
||||
width: 56,
|
||||
height: 56,
|
||||
marginRight: 12,
|
||||
},
|
||||
avatarGradient: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
position: "relative",
|
||||
},
|
||||
avatarInitials: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
textShadowColor: "rgba(0, 0, 0, 0.3)",
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
},
|
||||
avatarRing: {
|
||||
position: "absolute",
|
||||
top: -2,
|
||||
left: -2,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 18,
|
||||
borderWidth: 2,
|
||||
borderColor: "rgba(59, 130, 246, 0.2)",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
statusIndicator: {
|
||||
position: "absolute",
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#10b981",
|
||||
borderWidth: 3,
|
||||
borderColor: "white",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
memberCountBadge: {
|
||||
position: "absolute",
|
||||
top: -4,
|
||||
right: -4,
|
||||
backgroundColor: "#3b82f6",
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 2,
|
||||
borderColor: "white",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
memberCountText: {
|
||||
color: "white",
|
||||
fontSize: 11,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { useEventsByTablo, useCreateEvent } from "@/hooks/events";
|
|||
import { EventAndTablo, EventInsert } from "@/types/events.types";
|
||||
import { useTablosList } from "@/hooks/tablos";
|
||||
import { UserTablo } from "@/types/tablos.types";
|
||||
import { ColorMap } from "@/constants/colors";
|
||||
|
||||
type ViewMode = "month" | "week";
|
||||
|
||||
|
|
@ -54,7 +55,6 @@ const getDateFormatted = (date: Date) => {
|
|||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
console.log({ year, month, day });
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
|
|
@ -175,23 +175,6 @@ export default function PlanningScreen() {
|
|||
setShowCreateEventModal(false);
|
||||
};
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
"bg-blue-500": "#3b82f6",
|
||||
"bg-green-500": "#10b981",
|
||||
"bg-red-500": "#ef4444",
|
||||
"bg-yellow-500": "#f59e0b",
|
||||
"bg-purple-500": "#8b5cf6",
|
||||
"bg-pink-500": "#ec4899",
|
||||
"bg-gray-500": "#6b7280",
|
||||
"bg-orange-500": "#f5100b",
|
||||
"bg-teal-500": "#0d9488",
|
||||
"bg-indigo-500": "#6366f1",
|
||||
"bg-lime-500": "#84cc16",
|
||||
"bg-emerald-500": "#10b981",
|
||||
"bg-cyan-500": "#06b6d4",
|
||||
"bg-fuchsia-500": "#d946ef",
|
||||
};
|
||||
|
||||
const renderTabloOption = ({ item }: { item: UserTablo }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.tabloOption}
|
||||
|
|
@ -202,7 +185,7 @@ export default function PlanningScreen() {
|
|||
style={[
|
||||
styles.tabloColorDot,
|
||||
{
|
||||
backgroundColor: colorMap[item.color ?? "bg-gray-500"],
|
||||
backgroundColor: ColorMap[item.color ?? "bg-gray-500"],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -312,7 +295,7 @@ export default function PlanningScreen() {
|
|||
styles.weekEventCircle,
|
||||
{
|
||||
backgroundColor:
|
||||
colorMap[event.tablo_color ?? "bg-gray-500"],
|
||||
ColorMap[event.tablo_color ?? "bg-gray-500"],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -464,7 +447,7 @@ export default function PlanningScreen() {
|
|||
styles.tabloColorDot,
|
||||
{
|
||||
backgroundColor:
|
||||
colorMap[selectedTablo?.color ?? "bg-gray-500"],
|
||||
ColorMap[selectedTablo?.color ?? "bg-gray-500"],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -703,7 +686,7 @@ export default function PlanningScreen() {
|
|||
styles.tabloColorDot,
|
||||
{
|
||||
backgroundColor:
|
||||
colorMap[item.color ?? "bg-gray-500"],
|
||||
ColorMap[item.color ?? "bg-gray-500"],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
1046
xtablo-expo/app/(home)/(tabs)/tablos.tsx
Normal file
1046
xtablo-expo/app/(home)/(tabs)/tablos.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,38 +1,192 @@
|
|||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, SafeAreaView } from "react-native";
|
||||
import { Channel as ChannelType } from "stream-chat";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import {
|
||||
Channel,
|
||||
MessageInput,
|
||||
MessageList,
|
||||
useChatContext,
|
||||
useChannelContext,
|
||||
} from "stream-chat-expo";
|
||||
import { MessageCircle, Users, Smile } from "lucide-react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ChannelScreen() {
|
||||
const [channel, setChannel] = useState<ChannelType | null>(null);
|
||||
const { cid } = useLocalSearchParams<{ cid: string }>();
|
||||
|
||||
const [type, id] = cid.split(":");
|
||||
const { client } = useChatContext();
|
||||
const channel = client.channel(type, id);
|
||||
const [hasMessages, setHasMessages] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChannel = async () => {
|
||||
const channels = await client.queryChannels({ cid });
|
||||
setChannel(channels[0]);
|
||||
};
|
||||
fetchChannel();
|
||||
}, [cid]);
|
||||
if (channel) {
|
||||
const checkMessages = async () => {
|
||||
try {
|
||||
// Get channel state to check for messages
|
||||
await channel.watch();
|
||||
const messages = channel.state.messages || [];
|
||||
setHasMessages(messages.length > 0);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkMessages();
|
||||
|
||||
// Listen for new messages
|
||||
const handleNewMessage = () => {
|
||||
setHasMessages(true);
|
||||
};
|
||||
|
||||
channel.on("message.new", handleNewMessage);
|
||||
|
||||
return () => {
|
||||
channel.off("message.new", handleNewMessage);
|
||||
};
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
if (!channel) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
const EmptyState = () => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<MessageCircle size={64} color="#d1d5db" strokeWidth={1.5} />
|
||||
<View style={styles.decorativeElements}>
|
||||
<View style={styles.floatingIcon1}>
|
||||
<Users size={20} color="#e5e7eb" />
|
||||
</View>
|
||||
<View style={styles.floatingIcon2}>
|
||||
<Smile size={18} color="#e5e7eb" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.emptyTitle}>Commencez la conversation</Text>
|
||||
<Text style={styles.emptyMessage}>
|
||||
Soyez le premier à envoyer un message dans ce canal !
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<Channel channel={channel}>
|
||||
<MessageList />
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
<Text style={styles.loadingText}>Chargement des messages...</Text>
|
||||
</View>
|
||||
) : hasMessages ? (
|
||||
<MessageList />
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
<SafeAreaView>
|
||||
<MessageInput />
|
||||
</SafeAreaView>
|
||||
</Channel>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#f8fafc",
|
||||
gap: 16,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 16,
|
||||
color: "#6b7280",
|
||||
fontWeight: "500",
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 40,
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
emptyIconContainer: {
|
||||
position: "relative",
|
||||
marginBottom: 32,
|
||||
width: 120,
|
||||
height: 120,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
decorativeElements: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
floatingIcon1: {
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 5,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 15,
|
||||
padding: 6,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
floatingIcon2: {
|
||||
position: "absolute",
|
||||
bottom: 15,
|
||||
left: 8,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 12,
|
||||
padding: 5,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
marginBottom: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
emptyMessage: {
|
||||
fontSize: 16,
|
||||
color: "#6b7280",
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
marginBottom: 32,
|
||||
},
|
||||
emptyHint: {
|
||||
backgroundColor: "white",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "#e5e7eb",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 1,
|
||||
},
|
||||
emptyHintText: {
|
||||
fontSize: 14,
|
||||
color: "#9ca3af",
|
||||
fontWeight: "500",
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,70 @@
|
|||
// This is a shim for web and Android where the tab bar is generally opaque.
|
||||
export default undefined;
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
|
||||
export default function TabBarBackground() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<LinearGradient
|
||||
colors={
|
||||
isDark
|
||||
? ["#1F2937", "#111827", "#0F172A"]
|
||||
: ["#FFFFFF", "#F8FAFC", "#F1F5F9"]
|
||||
}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
{/* Subtle top border gradient */}
|
||||
<LinearGradient
|
||||
colors={
|
||||
isDark
|
||||
? ["rgba(59, 130, 246, 0.3)", "transparent"]
|
||||
: ["rgba(59, 130, 246, 0.1)", "transparent"]
|
||||
}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={styles.topBorder}
|
||||
/>
|
||||
|
||||
{/* Optional: Add some decorative elements */}
|
||||
<View
|
||||
style={[
|
||||
styles.decorativeCircle,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(59, 130, 246, 0.05)"
|
||||
: "rgba(59, 130, 246, 0.03)",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBottomTabOverflow() {
|
||||
return 0;
|
||||
return 24; // Account for the rounded corners
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
topBorder: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
},
|
||||
decorativeCircle: {
|
||||
position: "absolute",
|
||||
top: -20,
|
||||
right: 20,
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,24 +3,50 @@
|
|||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
const tintColorLight = '#0a7ea4';
|
||||
const tintColorDark = '#fff';
|
||||
const tintColorLight = "#0a7ea4";
|
||||
const tintColorDark = "#fff";
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: '#11181C',
|
||||
background: '#fff',
|
||||
text: "#11181C",
|
||||
background: "#fff",
|
||||
tint: tintColorLight,
|
||||
icon: '#687076',
|
||||
tabIconDefault: '#687076',
|
||||
icon: "#687076",
|
||||
tabIconDefault: "#687076",
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#ECEDEE',
|
||||
background: '#151718',
|
||||
text: "#ECEDEE",
|
||||
background: "#151718",
|
||||
tint: tintColorDark,
|
||||
icon: '#9BA1A6',
|
||||
tabIconDefault: '#9BA1A6',
|
||||
icon: "#9BA1A6",
|
||||
tabIconDefault: "#9BA1A6",
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
|
||||
export const AVAILABLE_COLORS = [
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-purple-500",
|
||||
"bg-red-500",
|
||||
"bg-yellow-500",
|
||||
"bg-indigo-500",
|
||||
"bg-pink-500",
|
||||
"bg-teal-500",
|
||||
"bg-orange-500",
|
||||
"bg-cyan-500",
|
||||
];
|
||||
|
||||
export const ColorMap: Record<(typeof AVAILABLE_COLORS)[number], string> = {
|
||||
"bg-blue-500": "#3b82f6",
|
||||
"bg-green-500": "#10b981",
|
||||
"bg-red-500": "#ef4444",
|
||||
"bg-yellow-500": "#f59e0b",
|
||||
"bg-purple-500": "#8b5cf6",
|
||||
"bg-pink-500": "#ec4899",
|
||||
"bg-orange-500": "#f97316",
|
||||
"bg-teal-500": "#0d9488",
|
||||
"bg-indigo-500": "#6366f1",
|
||||
"bg-cyan-500": "#06b6d4",
|
||||
};
|
||||
|
|
|
|||
52
xtablo-expo/constants/colors.ts
Normal file
52
xtablo-expo/constants/colors.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
|
||||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
const tintColorLight = "#0a7ea4";
|
||||
const tintColorDark = "#fff";
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: "#11181C",
|
||||
background: "#fff",
|
||||
tint: tintColorLight,
|
||||
icon: "#687076",
|
||||
tabIconDefault: "#687076",
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: "#ECEDEE",
|
||||
background: "#151718",
|
||||
tint: tintColorDark,
|
||||
icon: "#9BA1A6",
|
||||
tabIconDefault: "#9BA1A6",
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
|
||||
export const AVAILABLE_COLORS = [
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-purple-500",
|
||||
"bg-red-500",
|
||||
"bg-yellow-500",
|
||||
"bg-indigo-500",
|
||||
"bg-pink-500",
|
||||
"bg-teal-500",
|
||||
"bg-orange-500",
|
||||
"bg-cyan-500",
|
||||
];
|
||||
|
||||
export const ColorMap: Record<(typeof AVAILABLE_COLORS)[number], string> = {
|
||||
"bg-blue-500": "#3b82f6",
|
||||
"bg-green-500": "#10b981",
|
||||
"bg-red-500": "#ef4444",
|
||||
"bg-yellow-500": "#f59e0b",
|
||||
"bg-purple-500": "#8b5cf6",
|
||||
"bg-pink-500": "#ec4899",
|
||||
"bg-orange-500": "#f97316",
|
||||
"bg-teal-500": "#0d9488",
|
||||
"bg-indigo-500": "#6366f1",
|
||||
"bg-cyan-500": "#06b6d4",
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { TabloInsert, UserTablo } from "@/types/tablos.types";
|
||||
import { api } from "@/lib/api";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
// type TabloInsert = Tablo["Insert"];
|
||||
// type TabloUpdate = Tablo["Update"];
|
||||
|
|
@ -22,6 +23,7 @@ export const useTablosList = () => {
|
|||
const tablos = data as UserTablo[];
|
||||
return tablos;
|
||||
},
|
||||
refetchInterval: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ export const useCreateTablo = () => {
|
|||
queryClient.invalidateQueries({ queryKey: ["tablos"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
Alert.alert("Erreur", "Impossible de créer le tablo.");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { Colors } from "@/constants/colors";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const theme = useColorScheme() ?? "light";
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue