aboutsummaryrefslogtreecommitdiffstats
path: root/packages/mobile/components
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-13 21:43:44 +0000
committerMohamed Bassem <me@mbassem.com>2024-03-14 16:40:45 +0000
commit04572a8e5081b1e4871e273cde9dbaaa44c52fe0 (patch)
tree8e993acb732a50d1306d4d6953df96c165c57f57 /packages/mobile/components
parent2df08ed08c065e8b91bc8df0266bd4bcbb062be4 (diff)
downloadkarakeep-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.tsx11
-rw-r--r--packages/mobile/components/bookmarks/BookmarkCard.tsx243
-rw-r--r--packages/mobile/components/bookmarks/BookmarkList.tsx61
-rw-r--r--packages/mobile/components/ui/ActionButton.tsx21
-rw-r--r--packages/mobile/components/ui/Button.tsx81
-rw-r--r--packages/mobile/components/ui/Divider.tsx28
-rw-r--r--packages/mobile/components/ui/FullPageSpinner.tsx9
-rw-r--r--packages/mobile/components/ui/Input.tsx28
-rw-r--r--packages/mobile/components/ui/Skeleton.tsx38
-rw-r--r--packages/mobile/components/ui/Toast.tsx183
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 };