diff options
| author | MohamedBassem <me@mbassem.com> | 2024-03-13 21:43:44 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-03-14 16:40:45 +0000 |
| commit | 04572a8e5081b1e4871e273cde9dbaaa44c52fe0 (patch) | |
| tree | 8e993acb732a50d1306d4d6953df96c165c57f57 /packages/mobile/components | |
| parent | 2df08ed08c065e8b91bc8df0266bd4bcbb062be4 (diff) | |
| download | karakeep-04572a8e5081b1e4871e273cde9dbaaa44c52fe0.tar.zst | |
structure: Create apps dir and copy tooling dir from t3-turbo repo
Diffstat (limited to 'packages/mobile/components')
| -rw-r--r-- | packages/mobile/components/Logo.tsx | 11 | ||||
| -rw-r--r-- | packages/mobile/components/bookmarks/BookmarkCard.tsx | 243 | ||||
| -rw-r--r-- | packages/mobile/components/bookmarks/BookmarkList.tsx | 61 | ||||
| -rw-r--r-- | packages/mobile/components/ui/ActionButton.tsx | 21 | ||||
| -rw-r--r-- | packages/mobile/components/ui/Button.tsx | 81 | ||||
| -rw-r--r-- | packages/mobile/components/ui/Divider.tsx | 28 | ||||
| -rw-r--r-- | packages/mobile/components/ui/FullPageSpinner.tsx | 9 | ||||
| -rw-r--r-- | packages/mobile/components/ui/Input.tsx | 28 | ||||
| -rw-r--r-- | packages/mobile/components/ui/Skeleton.tsx | 38 | ||||
| -rw-r--r-- | packages/mobile/components/ui/Toast.tsx | 183 |
10 files changed, 0 insertions, 703 deletions
diff --git a/packages/mobile/components/Logo.tsx b/packages/mobile/components/Logo.tsx deleted file mode 100644 index 57f7a5c3..00000000 --- a/packages/mobile/components/Logo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PackageOpen } from "lucide-react-native"; -import { View, Text } from "react-native"; - -export default function Logo() { - return ( - <View className="flex flex-row items-center justify-center gap-2 "> - <PackageOpen color="black" size={70} /> - <Text className="text-5xl">Hoarder</Text> - </View> - ); -} diff --git a/packages/mobile/components/bookmarks/BookmarkCard.tsx b/packages/mobile/components/bookmarks/BookmarkCard.tsx deleted file mode 100644 index 25947790..00000000 --- a/packages/mobile/components/bookmarks/BookmarkCard.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import * as WebBrowser from "expo-web-browser"; -import { Star, Archive, Trash, ArchiveRestore } from "lucide-react-native"; -import { View, Text, Image, ScrollView, Pressable } from "react-native"; -import Markdown from "react-native-markdown-display"; - -import { ActionButton } from "../ui/ActionButton"; -import { Divider } from "../ui/Divider"; -import { Skeleton } from "../ui/Skeleton"; -import { useToast } from "../ui/Toast"; - -import { api } from "@/lib/trpc"; - -const MAX_LOADING_MSEC = 30 * 1000; - -export function isBookmarkStillCrawling(bookmark: ZBookmark) { - return ( - bookmark.content.type === "link" && - !bookmark.content.crawledAt && - Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC - ); -} - -export function isBookmarkStillTagging(bookmark: ZBookmark) { - return ( - bookmark.taggingStatus === "pending" && - Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC - ); -} - -export function isBookmarkStillLoading(bookmark: ZBookmark) { - return isBookmarkStillTagging(bookmark) || isBookmarkStillCrawling(bookmark); -} - -function ActionBar({ bookmark }: { bookmark: ZBookmark }) { - const { toast } = useToast(); - const apiUtils = api.useUtils(); - - const { mutate: deleteBookmark, isPending: isDeletionPending } = - api.bookmarks.deleteBookmark.useMutation({ - onSuccess: () => { - apiUtils.bookmarks.getBookmarks.invalidate(); - }, - onError: () => { - toast({ - message: "Something went wrong", - variant: "destructive", - showProgress: false, - }); - }, - }); - const { - mutate: updateBookmark, - variables, - isPending: isUpdatePending, - } = api.bookmarks.updateBookmark.useMutation({ - onSuccess: () => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: bookmark.id }); - }, - onError: () => { - toast({ - message: "Something went wrong", - variant: "destructive", - showProgress: false, - }); - }, - }); - - return ( - <View className="flex flex-row gap-4"> - <Pressable - onPress={() => - updateBookmark({ - bookmarkId: bookmark.id, - favourited: !bookmark.favourited, - }) - } - > - {(variables ? variables.favourited : bookmark.favourited) ? ( - <Star fill="#ebb434" color="#ebb434" /> - ) : ( - <Star color="gray" /> - )} - </Pressable> - <ActionButton - loading={isUpdatePending} - onPress={() => - updateBookmark({ - bookmarkId: bookmark.id, - archived: !bookmark.archived, - }) - } - > - {bookmark.archived ? ( - <ArchiveRestore color="gray" /> - ) : ( - <Archive color="gray" /> - )} - </ActionButton> - <ActionButton - loading={isDeletionPending} - onPress={() => - deleteBookmark({ - bookmarkId: bookmark.id, - }) - } - > - <Trash color="gray" /> - </ActionButton> - </View> - ); -} - -function TagList({ bookmark }: { bookmark: ZBookmark }) { - const tags = bookmark.tags; - - if (isBookmarkStillTagging(bookmark)) { - return ( - <> - <Skeleton className="h-4 w-full" /> - <Skeleton className="h-4 w-full" /> - </> - ); - } - - return ( - <ScrollView horizontal showsHorizontalScrollIndicator={false}> - <View className="flex flex-row gap-2"> - {tags.map((t) => ( - <View - key={t.id} - className="rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold" - > - <Text>{t.name}</Text> - </View> - ))} - </View> - </ScrollView> - ); -} - -function LinkCard({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type !== "link") { - throw new Error("Wrong content type rendered"); - } - - const url = bookmark.content.url; - const parsedUrl = new URL(url); - - const imageComp = bookmark.content.imageUrl ? ( - <Image - source={{ uri: bookmark.content.imageUrl }} - className="h-56 min-h-56 w-full rounded-t-lg object-cover" - /> - ) : ( - <Image - source={require("@/assets/blur.jpeg")} - className="h-56 w-full rounded-t-lg" - /> - ); - - return ( - <View className="flex gap-2"> - {imageComp} - <View className="flex gap-2 p-2"> - <Text - className="line-clamp-2 text-xl font-bold" - onPress={() => WebBrowser.openBrowserAsync(url)} - > - {bookmark.content.title || parsedUrl.host} - </Text> - <TagList bookmark={bookmark} /> - <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> - <View className="mt-2 flex flex-row justify-between px-2 pb-2"> - <Text className="my-auto line-clamp-1">{parsedUrl.host}</Text> - <ActionBar bookmark={bookmark} /> - </View> - </View> - </View> - ); -} - -function TextCard({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type !== "text") { - throw new Error("Wrong content type rendered"); - } - return ( - <View className="flex max-h-96 gap-2 p-2"> - <View className="max-h-56 overflow-hidden p-2"> - <Markdown>{bookmark.content.text}</Markdown> - </View> - <TagList bookmark={bookmark} /> - <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> - <View className="flex flex-row justify-between p-2"> - <View /> - <ActionBar bookmark={bookmark} /> - </View> - </View> - ); -} - -export default function BookmarkCard({ - bookmark: initialData, -}: { - bookmark: ZBookmark; -}) { - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: initialData.id, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - // If the link is not crawled or not tagged - if (isBookmarkStillLoading(data)) { - return 1000; - } - return false; - }, - }, - ); - - let comp; - switch (bookmark.content.type) { - case "link": - comp = <LinkCard bookmark={bookmark} />; - break; - case "text": - comp = <TextCard bookmark={bookmark} />; - break; - } - - return ( - <View className="w-96 rounded-lg border border-gray-300 bg-white shadow-sm"> - {comp} - </View> - ); -} diff --git a/packages/mobile/components/bookmarks/BookmarkList.tsx b/packages/mobile/components/bookmarks/BookmarkList.tsx deleted file mode 100644 index 8e408709..00000000 --- a/packages/mobile/components/bookmarks/BookmarkList.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useState } from "react"; -import { Text, View } from "react-native"; -import Animated, { LinearTransition } from "react-native-reanimated"; - -import BookmarkCard from "./BookmarkCard"; -import FullPageSpinner from "../ui/FullPageSpinner"; - -import { api } from "@/lib/trpc"; - -export default function BookmarkList({ - favourited, - archived, - ids, -}: { - favourited?: boolean; - archived?: boolean; - ids?: string[]; -}) { - const apiUtils = api.useUtils(); - const [refreshing, setRefreshing] = useState(false); - const { data, isPending, isPlaceholderData } = - api.bookmarks.getBookmarks.useQuery({ - favourited, - archived, - ids, - }); - - useEffect(() => { - setRefreshing(isPending || isPlaceholderData); - }, [isPending, isPlaceholderData]); - - if (isPending || !data) { - return <FullPageSpinner />; - } - - const onRefresh = () => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate(); - }; - - return ( - <Animated.FlatList - itemLayoutAnimation={LinearTransition} - contentContainerStyle={{ - gap: 15, - marginVertical: 15, - alignItems: "center", - }} - renderItem={(b) => <BookmarkCard bookmark={b.item} />} - ListEmptyComponent={ - <View className="h-full items-center justify-center"> - <Text className="text-xl">No Bookmarks</Text> - </View> - } - data={data.bookmarks} - refreshing={refreshing} - onRefresh={onRefresh} - keyExtractor={(b) => b.id} - /> - ); -} diff --git a/packages/mobile/components/ui/ActionButton.tsx b/packages/mobile/components/ui/ActionButton.tsx deleted file mode 100644 index c51eb332..00000000 --- a/packages/mobile/components/ui/ActionButton.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ActivityIndicator, Pressable, PressableProps } from "react-native"; - -export function ActionButton({ - children, - loading, - disabled, - ...props -}: PressableProps & { - loading: boolean; -}) { - if (disabled !== undefined) { - disabled ||= loading; - } else if (loading) { - disabled = true; - } - return ( - <Pressable {...props} disabled={disabled}> - {loading ? <ActivityIndicator /> : children} - </Pressable> - ); -} diff --git a/packages/mobile/components/ui/Button.tsx b/packages/mobile/components/ui/Button.tsx deleted file mode 100644 index 4c3cbc69..00000000 --- a/packages/mobile/components/ui/Button.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { type VariantProps, cva } from "class-variance-authority"; -import { Text, TouchableOpacity } from "react-native"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "flex flex-row items-center justify-center rounded-md", - { - variants: { - variant: { - default: "bg-primary", - secondary: "bg-secondary", - destructive: "bg-destructive", - ghost: "bg-slate-700", - link: "text-primary underline-offset-4", - }, - size: { - default: "h-10 px-4", - sm: "h-8 px-2", - lg: "h-12 px-8", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -const buttonTextVariants = cva("text-center font-medium", { - variants: { - variant: { - default: "text-primary-foreground", - secondary: "text-secondary-foreground", - destructive: "text-destructive-foreground", - ghost: "text-primary-foreground", - link: "text-primary-foreground underline", - }, - size: { - default: "text-base", - sm: "text-sm", - lg: "text-xl", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, -}); - -interface ButtonProps - extends React.ComponentPropsWithoutRef<typeof TouchableOpacity>, - VariantProps<typeof buttonVariants> { - label: string; - labelClasses?: string; -} -function Button({ - label, - labelClasses, - className, - variant, - size, - ...props -}: ButtonProps) { - return ( - <TouchableOpacity - className={cn(buttonVariants({ variant, size, className }))} - {...props} - > - <Text - className={cn( - buttonTextVariants({ variant, size, className: labelClasses }), - )} - > - {label} - </Text> - </TouchableOpacity> - ); -} - -export { Button, buttonVariants, buttonTextVariants }; diff --git a/packages/mobile/components/ui/Divider.tsx b/packages/mobile/components/ui/Divider.tsx deleted file mode 100644 index 1da0a71e..00000000 --- a/packages/mobile/components/ui/Divider.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { View } from "react-native"; - -import { cn } from "@/lib/utils"; - -function Divider({ - color = "#DFE4EA", - className, - orientation, - ...props -}: { - color?: string; - orientation: "horizontal" | "vertical"; -} & React.ComponentPropsWithoutRef<typeof View>) { - const dividerStyles = [{ backgroundColor: color }]; - - return ( - <View - className={cn( - orientation === "horizontal" ? "h-0.5" : "w-0.5", - className, - )} - style={dividerStyles} - {...props} - /> - ); -} - -export { Divider }; diff --git a/packages/mobile/components/ui/FullPageSpinner.tsx b/packages/mobile/components/ui/FullPageSpinner.tsx deleted file mode 100644 index 01187f11..00000000 --- a/packages/mobile/components/ui/FullPageSpinner.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { View, ActivityIndicator } from "react-native"; - -export default function FullPageSpinner() { - return ( - <View className="h-full w-full items-center justify-center"> - <ActivityIndicator /> - </View> - ); -} diff --git a/packages/mobile/components/ui/Input.tsx b/packages/mobile/components/ui/Input.tsx deleted file mode 100644 index 2fcb2764..00000000 --- a/packages/mobile/components/ui/Input.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { forwardRef } from "react"; -import { Text, TextInput, View } from "react-native"; - -import { cn } from "@/lib/utils"; - -export interface InputProps - extends React.ComponentPropsWithoutRef<typeof TextInput> { - label?: string; - labelClasses?: string; - inputClasses?: string; -} - -const Input = forwardRef<React.ElementRef<typeof TextInput>, InputProps>( - ({ className, label, labelClasses, inputClasses, ...props }, ref) => ( - <View className={cn("flex flex-col gap-1.5", className)}> - {label && <Text className={cn("text-base", labelClasses)}>{label}</Text>} - <TextInput - className={cn( - inputClasses, - "border-input rounded-lg border px-4 py-2.5", - )} - {...props} - /> - </View> - ), -); - -export { Input }; diff --git a/packages/mobile/components/ui/Skeleton.tsx b/packages/mobile/components/ui/Skeleton.tsx deleted file mode 100644 index 68b22e1e..00000000 --- a/packages/mobile/components/ui/Skeleton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect, useRef } from "react"; -import { Animated, type View } from "react-native"; - -import { cn } from "@/lib/utils"; - -function Skeleton({ - className, - ...props -}: { className?: string } & React.ComponentPropsWithoutRef<typeof View>) { - const fadeAnim = useRef(new Animated.Value(0.5)).current; - - useEffect(() => { - Animated.loop( - Animated.sequence([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 1000, - useNativeDriver: true, - }), - Animated.timing(fadeAnim, { - toValue: 0.5, - duration: 1000, - useNativeDriver: true, - }), - ]), - ).start(); - }, [fadeAnim]); - - return ( - <Animated.View - className={cn("bg-muted rounded-md", className)} - style={[{ opacity: fadeAnim }]} - {...props} - /> - ); -} - -export { Skeleton }; diff --git a/packages/mobile/components/ui/Toast.tsx b/packages/mobile/components/ui/Toast.tsx deleted file mode 100644 index fb319f84..00000000 --- a/packages/mobile/components/ui/Toast.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { createContext, useContext, useEffect, useRef, useState } from "react"; -import { Animated, Text, View } from "react-native"; - -import { cn } from "@/lib/utils"; - -const toastVariants = { - default: "bg-foreground", - destructive: "bg-destructive", - success: "bg-green-500", - info: "bg-blue-500", -}; - -interface ToastProps { - id: number; - message: string; - onHide: (id: number) => void; - variant?: keyof typeof toastVariants; - duration?: number; - showProgress?: boolean; -} -function Toast({ - id, - message, - onHide, - variant = "default", - duration = 3000, - showProgress = true, -}: ToastProps) { - const opacity = useRef(new Animated.Value(0)).current; - const progress = useRef(new Animated.Value(0)).current; - - useEffect(() => { - Animated.sequence([ - Animated.timing(opacity, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(progress, { - toValue: 1, - duration: duration - 1000, - useNativeDriver: false, - }), - Animated.timing(opacity, { - toValue: 0, - duration: 500, - useNativeDriver: true, - }), - ]).start(() => onHide(id)); - }, [duration]); - - return ( - <Animated.View - className={` - ${toastVariants[variant]} - m-2 mb-1 transform rounded-lg p-4 shadow-md transition-all - `} - style={{ - opacity, - transform: [ - { - translateY: opacity.interpolate({ - inputRange: [0, 1], - outputRange: [-20, 0], - }), - }, - ], - }} - > - <Text className="text-background text-left font-semibold">{message}</Text> - {showProgress && ( - <View className="mt-2 rounded"> - <Animated.View - className="h-2 rounded bg-white opacity-30 dark:bg-black" - style={{ - width: progress.interpolate({ - inputRange: [0, 1], - outputRange: ["0%", "100%"], - }), - }} - /> - </View> - )} - </Animated.View> - ); -} - -type ToastVariant = keyof typeof toastVariants; - -interface ToastMessage { - id: number; - text: string; - variant: ToastVariant; - duration?: number; - position?: string; - showProgress?: boolean; -} -interface ToastContextProps { - toast: (t: { - message: string; - variant?: keyof typeof toastVariants; - duration?: number; - position?: "top" | "bottom"; - showProgress?: boolean; - }) => void; - removeToast: (id: number) => void; -} -const ToastContext = createContext<ToastContextProps | undefined>(undefined); - -// TODO: refactor to pass position to Toast instead of ToastProvider -function ToastProvider({ - children, - position = "top", -}: { - children: React.ReactNode; - position?: "top" | "bottom"; -}) { - const [messages, setMessages] = useState<ToastMessage[]>([]); - - const toast: ToastContextProps["toast"] = ({ - message, - variant = "default", - duration = 3000, - position = "top", - showProgress = true, - }: { - message: string; - variant?: ToastVariant; - duration?: number; - position?: "top" | "bottom"; - showProgress?: boolean; - }) => { - setMessages((prev) => [ - ...prev, - { - id: Date.now(), - text: message, - variant, - duration, - position, - showProgress, - }, - ]); - }; - - const removeToast = (id: number) => { - setMessages((prev) => prev.filter((message) => message.id !== id)); - }; - - return ( - <ToastContext.Provider value={{ toast, removeToast }}> - {children} - <View - className={cn("absolute left-0 right-0", { - "top-[45px]": position === "top", - "bottom-0": position === "bottom", - })} - > - {messages.map((message) => ( - <Toast - key={message.id} - id={message.id} - message={message.text} - variant={message.variant} - duration={message.duration} - showProgress={message.showProgress} - onHide={removeToast} - /> - ))} - </View> - </ToastContext.Provider> - ); -} - -function useToast() { - const context = useContext(ToastContext); - if (!context) { - throw new Error("useToast must be used within ToastProvider"); - } - return context; -} - -export { ToastProvider, ToastVariant, Toast, toastVariants, useToast }; |
