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 | |
| 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')
49 files changed, 0 insertions, 1935 deletions
diff --git a/packages/mobile/.eslintrc.js b/packages/mobile/.eslintrc.js deleted file mode 100644 index 53beac49..00000000 --- a/packages/mobile/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ["universe/native"], -}; diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore deleted file mode 100644 index 2920e5a8..00000000 --- a/packages/mobile/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files - -# dependencies -node_modules/ - -# Expo -.expo/ -dist/ -web-build/ - -# Native -*.orig.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision - -# Metro -.metro-health-check* - -# debug -npm-debug.* -yarn-debug.* -yarn-error.* - -# macOS -.DS_Store -*.pem - -# local env files -.env*.local - -# typescript -*.tsbuildinfo - -#build files -ios/ -android/ diff --git a/packages/mobile/.npmrc b/packages/mobile/.npmrc deleted file mode 100644 index d67f3748..00000000 --- a/packages/mobile/.npmrc +++ /dev/null @@ -1 +0,0 @@ -node-linker=hoisted diff --git a/packages/mobile/app.json b/packages/mobile/app.json deleted file mode 100644 index e16baa37..00000000 --- a/packages/mobile/app.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "expo": { - "name": "Hoarder App", - "slug": "hoarder", - "scheme": "hoarder", - "version": "1.2.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "light", - "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true, - "bundleIdentifier": "app.hoarder.hoardermobile", - "config": { - "usesNonExemptEncryption": false - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/icon.png", - "backgroundColor": "#ffffff" - }, - "package": "app.hoarder.hoardermobile" - }, - "plugins": [ - "expo-router", - [ - "expo-share-intent", - { - "iosActivationRules": { - "NSExtensionActivationSupportsWebURLWithMaxCount": 1, - "NSExtensionActivationSupportsWebPageWithMaxCount": 0, - "NSExtensionActivationSupportsImageWithMaxCount": 0, - "NSExtensionActivationSupportsMovieWithMaxCount": 0, - "NSExtensionActivationSupportsText": true - } - } - ], - "expo-secure-store" - ], - "extra": { - "router": { - "origin": false - }, - "eas": { - "projectId": "d6d14643-ad43-4cd3-902a-92c5944d5e45" - } - } - } -} diff --git a/packages/mobile/app/+not-found.tsx b/packages/mobile/app/+not-found.tsx deleted file mode 100644 index 466505b6..00000000 --- a/packages/mobile/app/+not-found.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { View } from "react-native"; - -// This is kinda important given that the sharing modal always resolve to an unknown route -export default function NotFound() { - return <View />; -} diff --git a/packages/mobile/app/_layout.tsx b/packages/mobile/app/_layout.tsx deleted file mode 100644 index 6304ced5..00000000 --- a/packages/mobile/app/_layout.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import "@/globals.css"; -import "expo-dev-client"; - -import { useRouter } from "expo-router"; -import { Stack } from "expo-router/stack"; -import { useShareIntent } from "expo-share-intent"; -import { StatusBar } from "expo-status-bar"; -import { useEffect } from "react"; -import { View } from "react-native"; - -import { useLastSharedIntent } from "@/lib/last-shared-intent"; -import { Providers } from "@/lib/providers"; - -export default function RootLayout() { - const router = useRouter(); - const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent(); - - const lastSharedIntent = useLastSharedIntent(); - - useEffect(() => { - const intentJson = JSON.stringify(shareIntent); - if (hasShareIntent && !lastSharedIntent.isPreviouslyShared(intentJson)) { - // TODO: Remove once https://github.com/achorein/expo-share-intent/issues/14 is fixed - lastSharedIntent.setIntent(intentJson); - router.replace({ - pathname: "sharing", - params: { shareIntent: intentJson }, - }); - resetShareIntent(); - } - }, [hasShareIntent]); - - return ( - <Providers> - <View className="h-full w-full bg-white"> - <Stack - screenOptions={{ - headerShown: false, - }} - > - <Stack.Screen name="index" /> - <Stack.Screen - name="sharing" - options={{ - presentation: "modal", - }} - /> - </Stack> - <StatusBar style="auto" /> - </View> - </Providers> - ); -} diff --git a/packages/mobile/app/dashboard/(tabs)/_layout.tsx b/packages/mobile/app/dashboard/(tabs)/_layout.tsx deleted file mode 100644 index 5b2d810a..00000000 --- a/packages/mobile/app/dashboard/(tabs)/_layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Tabs } from "expo-router"; -import { ClipboardList, Home, Search, Settings } from "lucide-react-native"; -import React from "react"; - -export default function TabLayout() { - return ( - <Tabs screenOptions={{ tabBarActiveTintColor: "blue" }}> - <Tabs.Screen - name="index" - options={{ - title: "Home", - tabBarIcon: ({ color }) => <Home color={color} />, - }} - /> - <Tabs.Screen - name="search" - options={{ - title: "Search", - tabBarIcon: ({ color }) => <Search color={color} />, - }} - /> - <Tabs.Screen - name="lists" - options={{ - title: "Lists", - tabBarIcon: ({ color }) => <ClipboardList color={color} />, - }} - /> - <Tabs.Screen - name="settings" - options={{ - title: "Settings", - tabBarIcon: ({ color }) => <Settings color={color} />, - }} - /> - </Tabs> - ); -} diff --git a/packages/mobile/app/dashboard/(tabs)/index.tsx b/packages/mobile/app/dashboard/(tabs)/index.tsx deleted file mode 100644 index b2349525..00000000 --- a/packages/mobile/app/dashboard/(tabs)/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Link, Stack } from "expo-router"; -import { SquarePen, Link as LinkIcon } from "lucide-react-native"; -import { View } from "react-native"; - -import BookmarkList from "@/components/bookmarks/BookmarkList"; - -function HeaderRight() { - return ( - <View className="flex flex-row"> - <Link href="dashboard/add-link" className="mt-2 px-2"> - <LinkIcon /> - </Link> - <Link href="dashboard/add-note" className="mt-2 px-2"> - <SquarePen /> - </Link> - </View> - ); -} - -export default function Home() { - return ( - <> - <Stack.Screen - options={{ - headerRight: () => <HeaderRight />, - }} - /> - <BookmarkList archived={false} /> - </> - ); -} diff --git a/packages/mobile/app/dashboard/(tabs)/lists.tsx b/packages/mobile/app/dashboard/(tabs)/lists.tsx deleted file mode 100644 index b534ddda..00000000 --- a/packages/mobile/app/dashboard/(tabs)/lists.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Link } from "expo-router"; -import { useEffect, useState } from "react"; -import { FlatList, View } from "react-native"; - -import { api } from "@/lib/trpc"; - -export default function Lists() { - const [refreshing, setRefreshing] = useState(false); - const { data: lists, isPending } = api.lists.list.useQuery(); - const apiUtils = api.useUtils(); - - useEffect(() => { - setRefreshing(isPending); - }, [isPending]); - - if (!lists) { - // Add spinner - return <View />; - } - - const onRefresh = () => { - apiUtils.lists.list.invalidate(); - }; - - const links = [ - { - id: "fav", - logo: "⭐️", - name: "Favourites", - href: "/dashboard/favourites", - }, - { - id: "arch", - logo: "🗄️", - name: "Archive", - href: "/dashboard/archive", - }, - ]; - - links.push( - ...lists.lists.map((l) => ({ - id: l.id, - logo: l.icon, - name: l.name, - href: `/dashboard/lists/${l.id}`, - })), - ); - - return ( - <FlatList - contentContainerStyle={{ - gap: 10, - marginTop: 10, - }} - renderItem={(l) => ( - <View className="mx-2 block rounded-xl border border-gray-100 bg-white px-4 py-2"> - <Link key={l.item.id} href={l.item.href} className="text-lg"> - {l.item.logo} {l.item.name} - </Link> - </View> - )} - data={links} - refreshing={refreshing} - onRefresh={onRefresh} - /> - ); -} diff --git a/packages/mobile/app/dashboard/(tabs)/search.tsx b/packages/mobile/app/dashboard/(tabs)/search.tsx deleted file mode 100644 index 980cab36..00000000 --- a/packages/mobile/app/dashboard/(tabs)/search.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { keepPreviousData } from "@tanstack/react-query"; -import { useState } from "react"; -import { View } from "react-native"; -import { useDebounce } from "use-debounce"; - -import BookmarkList from "@/components/bookmarks/BookmarkList"; -import { Divider } from "@/components/ui/Divider"; -import { Input } from "@/components/ui/Input"; -import { api } from "@/lib/trpc"; - -export default function Search() { - const [search, setSearch] = useState(""); - - const [query] = useDebounce(search, 200); - - const { data } = api.bookmarks.searchBookmarks.useQuery( - { text: query }, - { placeholderData: keepPreviousData }, - ); - - return ( - <View> - <Input - placeholder="Search" - className="mx-4 mt-4 bg-white" - value={search} - onChangeText={setSearch} - autoFocus - autoCapitalize="none" - /> - <Divider orientation="horizontal" className="mb-1 mt-4 w-full" /> - {data && <BookmarkList ids={data.bookmarks.map((b) => b.id)} />} - </View> - ); -} diff --git a/packages/mobile/app/dashboard/(tabs)/settings.tsx b/packages/mobile/app/dashboard/(tabs)/settings.tsx deleted file mode 100644 index 9f86d5ec..00000000 --- a/packages/mobile/app/dashboard/(tabs)/settings.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useRouter } from "expo-router"; -import { useEffect } from "react"; -import { Text, View } from "react-native"; - -import Logo from "@/components/Logo"; -import { Button } from "@/components/ui/Button"; -import { useSession } from "@/lib/session"; -import { api } from "@/lib/trpc"; - -export default function Dashboard() { - const router = useRouter(); - - const { isLoggedIn, logout } = useSession(); - - useEffect(() => { - if (isLoggedIn !== undefined && !isLoggedIn) { - router.replace("signin"); - } - }, [isLoggedIn]); - - const { data, error, isLoading } = api.users.whoami.useQuery(); - - useEffect(() => { - if (error?.data?.code === "UNAUTHORIZED") { - logout(); - } - }, [error]); - - return ( - <View className="flex h-full w-full items-center gap-4 p-4"> - <Logo /> - <View className="w-full rounded-lg bg-white px-4 py-2"> - <Text className="text-lg"> - {isLoading ? "Loading ..." : data?.email} - </Text> - </View> - - <Button className="w-full" label="Log Out" onPress={logout} /> - </View> - ); -} diff --git a/packages/mobile/app/dashboard/_layout.tsx b/packages/mobile/app/dashboard/_layout.tsx deleted file mode 100644 index ff2384d2..00000000 --- a/packages/mobile/app/dashboard/_layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Stack } from "expo-router/stack"; - -export default function Dashboard() { - return ( - <Stack> - <Stack.Screen - name="(tabs)" - options={{ headerShown: false, title: "Home" }} - /> - <Stack.Screen - name="favourites" - options={{ - title: "⭐️ Favourites", - }} - /> - <Stack.Screen - name="archive" - options={{ - title: "🗄️ Archive", - }} - /> - <Stack.Screen - name="add-link" - options={{ - title: "New link", - presentation: "modal", - }} - /> - <Stack.Screen - name="add-note" - options={{ - title: "New Note", - presentation: "modal", - }} - /> - </Stack> - ); -} diff --git a/packages/mobile/app/dashboard/add-link.tsx b/packages/mobile/app/dashboard/add-link.tsx deleted file mode 100644 index 69a9c7a2..00000000 --- a/packages/mobile/app/dashboard/add-link.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useRouter } from "expo-router"; -import { useState } from "react"; -import { View, Text } from "react-native"; - -import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; -import { api } from "@/lib/trpc"; - -export default function AddNote() { - const [text, setText] = useState(""); - const [error, setError] = useState<string | undefined>(); - const router = useRouter(); - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - - const { mutate } = api.bookmarks.createBookmark.useMutation({ - onSuccess: () => { - invalidateAllBookmarks(); - if (router.canGoBack()) { - router.replace("../"); - } else { - router.replace("dashboard"); - } - }, - onError: (e) => { - let message; - if (e.data?.code === "BAD_REQUEST") { - const error = JSON.parse(e.message)[0]; - message = error.message; - } else { - message = `Something went wrong: ${e.message}`; - } - setError(message); - }, - }); - - return ( - <View className="flex gap-2 p-4"> - {error && ( - <Text className="w-full text-center text-red-500">{error}</Text> - )} - <Input - className="bg-white" - value={text} - onChangeText={setText} - placeholder="Link" - autoCapitalize="none" - inputMode="url" - autoFocus - /> - <Button - onPress={() => mutate({ type: "link", url: text })} - label="Add Link" - /> - </View> - ); -} diff --git a/packages/mobile/app/dashboard/add-note.tsx b/packages/mobile/app/dashboard/add-note.tsx deleted file mode 100644 index cf775a15..00000000 --- a/packages/mobile/app/dashboard/add-note.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useRouter } from "expo-router"; -import { useState } from "react"; -import { View, Text } from "react-native"; - -import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; -import { api } from "@/lib/trpc"; - -export default function AddNote() { - const [text, setText] = useState(""); - const [error, setError] = useState<string | undefined>(); - const router = useRouter(); - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - - const { mutate } = api.bookmarks.createBookmark.useMutation({ - onSuccess: () => { - invalidateAllBookmarks(); - if (router.canGoBack()) { - router.replace("../"); - } else { - router.replace("dashboard"); - } - }, - onError: (e) => { - let message; - if (e.data?.code === "BAD_REQUEST") { - const error = JSON.parse(e.message)[0]; - message = error.message; - } else { - message = `Something went wrong: ${e.message}`; - } - setError(message); - }, - }); - - return ( - <View className="flex gap-2 p-4"> - {error && ( - <Text className="w-full text-center text-red-500">{error}</Text> - )} - <Input - className="bg-white" - value={text} - onChangeText={setText} - multiline - placeholder="What's on your mind?" - autoFocus - /> - <Button onPress={() => mutate({ type: "text", text })} label="Add Note" /> - </View> - ); -} diff --git a/packages/mobile/app/dashboard/archive.tsx b/packages/mobile/app/dashboard/archive.tsx deleted file mode 100644 index d75cfe22..00000000 --- a/packages/mobile/app/dashboard/archive.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { View } from "react-native"; - -import BookmarkList from "@/components/bookmarks/BookmarkList"; - -export default function Archive() { - return ( - <View> - <BookmarkList archived /> - </View> - ); -} diff --git a/packages/mobile/app/dashboard/favourites.tsx b/packages/mobile/app/dashboard/favourites.tsx deleted file mode 100644 index 90374f18..00000000 --- a/packages/mobile/app/dashboard/favourites.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { View } from "react-native"; - -import BookmarkList from "@/components/bookmarks/BookmarkList"; - -export default function Favourites() { - return ( - <View> - <BookmarkList archived={false} favourited /> - </View> - ); -} diff --git a/packages/mobile/app/dashboard/lists/[slug].tsx b/packages/mobile/app/dashboard/lists/[slug].tsx deleted file mode 100644 index 54744874..00000000 --- a/packages/mobile/app/dashboard/lists/[slug].tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useLocalSearchParams, Stack } from "expo-router"; -import { View } from "react-native"; - -import BookmarkList from "@/components/bookmarks/BookmarkList"; -import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { api } from "@/lib/trpc"; - -export default function ListView() { - const { slug } = useLocalSearchParams(); - if (typeof slug !== "string") { - throw new Error("Unexpected param type"); - } - const { data: list } = api.lists.get.useQuery({ listId: slug }); - - if (!list) { - return <FullPageSpinner />; - } - - return ( - <> - <Stack.Screen - options={{ - headerTitle: `${list.icon} ${list.name}`, - }} - /> - <View> - <BookmarkList archived={false} ids={list.bookmarks} /> - </View> - </> - ); -} diff --git a/packages/mobile/app/error.tsx b/packages/mobile/app/error.tsx deleted file mode 100644 index 2ca227a4..00000000 --- a/packages/mobile/app/error.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { View, Text } from "react-native"; - -export default function ErrorPage() { - return ( - <View className="flex-1 items-center justify-center gap-4"> - <Text className="text-4xl">Error!</Text> - </View> - ); -} diff --git a/packages/mobile/app/index.tsx b/packages/mobile/app/index.tsx deleted file mode 100644 index 5ce20cda..00000000 --- a/packages/mobile/app/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useRouter } from "expo-router"; -import { useEffect } from "react"; -import { View } from "react-native"; - -import { useSession } from "@/lib/session"; - -export default function App() { - const router = useRouter(); - const { isLoggedIn } = useSession(); - useEffect(() => { - if (isLoggedIn === undefined) { - // Wait until it's loaded - } else if (isLoggedIn) { - router.replace("dashboard"); - } else { - router.replace("signin"); - } - }, [isLoggedIn]); - return <View />; -} diff --git a/packages/mobile/app/sharing.tsx b/packages/mobile/app/sharing.tsx deleted file mode 100644 index 64bbd933..00000000 --- a/packages/mobile/app/sharing.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Link, useLocalSearchParams, useRouter } from "expo-router"; -import { ShareIntent, useShareIntent } from "expo-share-intent"; -import { useEffect, useMemo, useState } from "react"; -import { View, Text } from "react-native"; -import { z } from "zod"; - -import { api } from "@/lib/trpc"; - -type Mode = - | { type: "idle" } - | { type: "success"; bookmarkId: string } - | { type: "error" }; - -function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { - // Desperate attempt to fix sharing duplication - const { hasShareIntent, resetShareIntent } = useShareIntent(); - - const params = useLocalSearchParams(); - - const shareIntent = useMemo(() => { - if (params && params.shareIntent) { - if (typeof params.shareIntent === "string") { - return JSON.parse(params.shareIntent) as ShareIntent; - } - } - return null; - }, [params]); - - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - - useEffect(() => { - if (!isPending && shareIntent?.text) { - const val = z.string().url(); - if (val.safeParse(shareIntent.text).success) { - // This is a URL, else treated as text - mutate({ type: "link", url: shareIntent.text }); - } else { - mutate({ type: "text", text: shareIntent.text }); - } - } - if (hasShareIntent) { - resetShareIntent(); - } - }, []); - - const { mutate, isPending } = api.bookmarks.createBookmark.useMutation({ - onSuccess: (d) => { - invalidateAllBookmarks(); - setMode({ type: "success", bookmarkId: d.id }); - }, - onError: () => { - setMode({ type: "error" }); - }, - }); - - return <Text className="text-4xl">Hoarding ...</Text>; -} - -export default function Sharing() { - const router = useRouter(); - const [mode, setMode] = useState<Mode>({ type: "idle" }); - - let comp; - switch (mode.type) { - case "idle": { - comp = <SaveBookmark setMode={setMode} />; - break; - } - case "success": { - comp = <Text className="text-4xl">Hoarded!</Text>; - break; - } - case "error": { - comp = <Text className="text-4xl">Error!</Text>; - break; - } - } - - // Auto dismiss the modal after saving. - useEffect(() => { - if (mode.type === "idle") { - return; - } - - const timeoutId = setTimeout(() => { - router.replace("dashboard"); - }, 2000); - - return () => clearTimeout(timeoutId); - }, [mode.type]); - - return ( - <View className="flex-1 items-center justify-center gap-4"> - {comp} - <Link href="dashboard">Dismiss</Link> - </View> - ); -} diff --git a/packages/mobile/app/signin.tsx b/packages/mobile/app/signin.tsx deleted file mode 100644 index a89b0087..00000000 --- a/packages/mobile/app/signin.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useRouter } from "expo-router"; -import { useEffect, useState } from "react"; -import { View, Text } from "react-native"; - -import Logo from "@/components/Logo"; -import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; -import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; - -export default function Signin() { - const router = useRouter(); - - const { settings, setSettings } = useAppSettings(); - - const [error, setError] = useState<string | undefined>(); - - const { mutate: login, isPending } = api.apiKeys.exchange.useMutation({ - onSuccess: (resp) => { - setSettings({ ...settings, apiKey: resp.key }); - router.replace("dashboard"); - }, - onError: (e) => { - if (e.data?.code === "UNAUTHORIZED") { - setError("Wrong username or password"); - } else { - setError(`${e.message}`); - } - }, - }); - - const [formData, setFormData] = useState<{ - email: string; - password: string; - }>({ - email: "", - password: "", - }); - - useEffect(() => { - if (settings.apiKey) { - router.navigate("dashboard"); - } - }, [settings]); - - const onSignin = () => { - const randStr = (Math.random() + 1).toString(36).substring(5); - login({ ...formData, keyName: `Mobile App: (${randStr})` }); - }; - - return ( - <View className="flex h-full flex-col justify-center gap-2 px-4"> - <View className="items-center"> - <Logo /> - </View> - {error && ( - <Text className="w-full text-center text-red-500">{error}</Text> - )} - <View className="gap-2"> - <Text className="font-bold">Server Address</Text> - <Input - className="w-full" - placeholder="Server Address" - value={settings.address} - autoCapitalize="none" - keyboardType="url" - onEndEditing={(e) => - setSettings({ ...settings, address: e.nativeEvent.text }) - } - /> - </View> - <View className="gap-2"> - <Text className="font-bold">Email</Text> - <Input - className="w-full" - placeholder="Email" - keyboardType="email-address" - autoCapitalize="none" - value={formData.email} - onChangeText={(e) => setFormData((s) => ({ ...s, email: e }))} - /> - </View> - <View className="gap-2"> - <Text className="font-bold">Password</Text> - <Input - className="w-full" - placeholder="Password" - secureTextEntry - value={formData.password} - onChangeText={(e) => setFormData((s) => ({ ...s, password: e }))} - /> - </View> - <Button - className="w-full" - label="Sign In" - onPress={onSignin} - disabled={isPending} - /> - </View> - ); -} diff --git a/packages/mobile/assets/blur.jpeg b/packages/mobile/assets/blur.jpeg Binary files differdeleted file mode 100644 index 387ce697..00000000 --- a/packages/mobile/assets/blur.jpeg +++ /dev/null diff --git a/packages/mobile/assets/icon.png b/packages/mobile/assets/icon.png Binary files differdeleted file mode 100644 index 71ead90c..00000000 --- a/packages/mobile/assets/icon.png +++ /dev/null diff --git a/packages/mobile/assets/splash.png b/packages/mobile/assets/splash.png Binary files differdeleted file mode 100644 index 3759c518..00000000 --- a/packages/mobile/assets/splash.png +++ /dev/null diff --git a/packages/mobile/babel.config.js b/packages/mobile/babel.config.js deleted file mode 100644 index f3c649bb..00000000 --- a/packages/mobile/babel.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function (api) { - api.cache(true); - return { - presets: [ - ["babel-preset-expo", { jsxImportSource: "nativewind" }], - "nativewind/babel", - ], - }; -}; 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 }; diff --git a/packages/mobile/eas.json b/packages/mobile/eas.json deleted file mode 100644 index 0897755d..00000000 --- a/packages/mobile/eas.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "cli": { - "version": ">= 7.5.0", - "promptToConfigurePushNotifications": false - }, - "build": { - "development": { - "developmentClient": true, - "distribution": "internal" - }, - "preview": { - "distribution": "internal" - }, - "production": {} - }, - "submit": { - "production": {} - } -} diff --git a/packages/mobile/globals.css b/packages/mobile/globals.css deleted file mode 100644 index de1cf559..00000000 --- a/packages/mobile/globals.css +++ /dev/null @@ -1,80 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - - --destructive: 0 100% 50%; - --destructive-foreground: 210 40% 98%; - - --ring: 215 20.2% 65.1%; - - --radius: 0.5rem; - } - - .dark:root { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; - - --accent: 216 34% 17%; - --accent-foreground: 210 40% 98%; - - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; - - --border: 216 34% 17%; - --input: 216 34% 17%; - - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; - - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 210 40% 98%; - - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; - - --ring: 216 34% 17%; - - --radius: 0.5rem; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/packages/mobile/lib/last-shared-intent.ts b/packages/mobile/lib/last-shared-intent.ts deleted file mode 100644 index 951bcf74..00000000 --- a/packages/mobile/lib/last-shared-intent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from "zustand"; - -interface LastSharedIntent { - lastIntent: string; - setIntent: (intent: string) => void; - isPreviouslyShared: (intent: string) => boolean; -} - -export const useLastSharedIntent = create<LastSharedIntent>((set, get) => ({ - lastIntent: "", - setIntent: (intent: string) => set({ lastIntent: intent }), - isPreviouslyShared: (intent: string) => { - return get().lastIntent === intent; - }, -})); diff --git a/packages/mobile/lib/providers.tsx b/packages/mobile/lib/providers.tsx deleted file mode 100644 index 1717afb2..00000000 --- a/packages/mobile/lib/providers.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { httpBatchLink } from "@trpc/client"; -import { useEffect, useState } from "react"; -import superjson from "superjson"; - -import useAppSettings, { getAppSettings } from "./settings"; -import { api } from "./trpc"; - -import { ToastProvider } from "@/components/ui/Toast"; - -function getTRPCClient(address: string) { - return api.createClient({ - links: [ - httpBatchLink({ - url: `${address}/api/trpc`, - async headers() { - const settings = await getAppSettings(); - return { - Authorization: - settings && settings.apiKey - ? `Bearer ${settings.apiKey}` - : undefined, - }; - }, - transformer: superjson, - }), - ], - }); -} - -export function Providers({ children }: { children: React.ReactNode }) { - const { settings } = useAppSettings(); - const [queryClient] = useState(() => new QueryClient()); - - const [trpcClient, setTrpcClient] = useState< - ReturnType<typeof getTRPCClient> - >(getTRPCClient(settings.address)); - - useEffect(() => { - setTrpcClient(getTRPCClient(settings.address)); - }, [settings.address]); - - return ( - <api.Provider - key={settings.address} - client={trpcClient} - queryClient={queryClient} - > - <QueryClientProvider client={queryClient}> - <ToastProvider>{children}</ToastProvider> - </QueryClientProvider> - </api.Provider> - ); -} diff --git a/packages/mobile/lib/session.ts b/packages/mobile/lib/session.ts deleted file mode 100644 index e2ab245b..00000000 --- a/packages/mobile/lib/session.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useCallback, useMemo } from "react"; - -import useAppSettings from "./settings"; - -export function useSession() { - const { settings, isLoading, setSettings } = useAppSettings(); - const isLoggedIn = useMemo(() => { - return isLoading ? undefined : !!settings.apiKey; - }, [isLoading, settings]); - - const logout = useCallback(() => { - setSettings({ ...settings, apiKey: undefined }); - }, [settings]); - - return { - isLoggedIn, - isLoading, - logout, - }; -} diff --git a/packages/mobile/lib/settings.ts b/packages/mobile/lib/settings.ts deleted file mode 100644 index 21f40528..00000000 --- a/packages/mobile/lib/settings.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as SecureStore from "expo-secure-store"; - -import { useStorageState } from "./storage-state"; - -const SETTING_NAME = "settings"; - -export type Settings = { - apiKey?: string; - address: string; -}; - -export default function useAppSettings() { - let [[isLoading, settings], setSettings] = - useStorageState<Settings>(SETTING_NAME); - - settings ||= { - address: "https://demo.hoarder.app", - }; - - return { settings, setSettings, isLoading }; -} - -export async function getAppSettings() { - const val = await SecureStore.getItemAsync(SETTING_NAME); - if (!val) { - return null; - } - return JSON.parse(val) as Settings; -} diff --git a/packages/mobile/lib/storage-state.ts b/packages/mobile/lib/storage-state.ts deleted file mode 100644 index 4988f0e0..00000000 --- a/packages/mobile/lib/storage-state.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as SecureStore from "expo-secure-store"; -import * as React from "react"; - -type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void]; - -function useAsyncState<T>( - initialValue: [boolean, T | null] = [true, null], -): UseStateHook<T> { - return React.useReducer( - ( - state: [boolean, T | null], - action: T | null = null, - ): [boolean, T | null] => [false, action], - initialValue, - ) as UseStateHook<T>; -} - -export async function setStorageItemAsync(key: string, value: string | null) { - if (value == null) { - await SecureStore.deleteItemAsync(key); - } else { - await SecureStore.setItemAsync(key, value); - } -} - -export function useStorageState<T>(key: string): UseStateHook<T> { - // Public - const [state, setState] = useAsyncState<T>(); - - // Get - React.useEffect(() => { - SecureStore.getItemAsync(key).then((value) => { - if (!value) { - setState(null); - return null; - } - setState(JSON.parse(value)); - }); - }, [key]); - - // Set - const setValue = React.useCallback( - (value: T | null) => { - setState(value); - setStorageItemAsync(key, JSON.stringify(value)); - }, - [key], - ); - - return [state, setValue]; -} diff --git a/packages/mobile/lib/trpc.ts b/packages/mobile/lib/trpc.ts deleted file mode 100644 index 6b428bd9..00000000 --- a/packages/mobile/lib/trpc.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { AppRouter } from "@hoarder/trpc/routers/_app"; -import { createTRPCReact } from "@trpc/react-query"; - -export const api = createTRPCReact<AppRouter>(); diff --git a/packages/mobile/lib/utils.ts b/packages/mobile/lib/utils.ts deleted file mode 100644 index 365058ce..00000000 --- a/packages/mobile/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js deleted file mode 100644 index 6b2b0477..00000000 --- a/packages/mobile/metro.config.js +++ /dev/null @@ -1,8 +0,0 @@ -const { getDefaultConfig } = require("expo/metro-config"); -const { withNativeWind } = require("nativewind/metro"); - -/** @type {import('expo/metro-config').MetroConfig} */ -// eslint-disable-next-line no-undef -const config = getDefaultConfig(__dirname); - -module.exports = withNativeWind(config, { input: "./globals.css" }); diff --git a/packages/mobile/nativewind-env.d.ts b/packages/mobile/nativewind-env.d.ts deleted file mode 100644 index a13e3136..00000000 --- a/packages/mobile/nativewind-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="nativewind/types" /> diff --git a/packages/mobile/package.json b/packages/mobile/package.json deleted file mode 100644 index 1298b8db..00000000 --- a/packages/mobile/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "hoarder-mobile", - "version": "1.0.0", - "main": "expo-router/entry", - "scripts": { - "start": "expo start", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "expo start --web", - "lint": "eslint ." - }, - "dependencies": { - "@hoarder/trpc": "0.1.0", - "@tanstack/react-query": "^5.24.8", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "expo": "~50.0.11", - "expo-config-plugin-ios-share-extension": "^0.0.4", - "expo-constants": "~15.4.5", - "expo-dev-client": "^3.3.9", - "expo-image": "^1.10.6", - "expo-linking": "~6.2.2", - "expo-router": "~3.4.8", - "expo-secure-store": "^12.8.1", - "expo-share-intent": "^1.0.1", - "expo-status-bar": "~1.11.1", - "expo-web-browser": "^12.8.2", - "lucide-react-native": "^0.354.0", - "nativewind": "^4.0.1", - "react": "18.2.0", - "react-native": "0.73.4", - "react-native-markdown-display": "^7.0.2", - "react-native-reanimated": "^3.8.0", - "react-native-safe-area-context": "4.8.2", - "react-native-screens": "~3.29.0", - "react-native-svg": "^15.1.0", - "tailwind-merge": "^2.2.1", - "use-debounce": "^10.0.0", - "zod": "^3.22.4", - "zustand": "^4.5.1" - }, - "devDependencies": { - "@babel/core": "^7.20.0", - "@types/react": "~18.2.45", - "ajv": "latest", - "eslint": "^8.57.0", - "eslint-config-universe": "^12.0.0", - "prettier": "^3.2.5", - "tailwindcss": "3.3.2", - "typescript": "^5.1.3" - }, - "private": true -} diff --git a/packages/mobile/tailwind.config.js b/packages/mobile/tailwind.config.js deleted file mode 100644 index b49f9598..00000000 --- a/packages/mobile/tailwind.config.js +++ /dev/null @@ -1,71 +0,0 @@ -const { hairlineWidth } = require("nativewind/theme"); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], - plugins: [], - presets: [require("nativewind/preset")], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderWidth: { - hairline: hairlineWidth(), - }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, -}; diff --git a/packages/mobile/tsconfig.json b/packages/mobile/tsconfig.json deleted file mode 100644 index 84d97cb0..00000000 --- a/packages/mobile/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": { - "strict": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"] - } - } -} |
