aboutsummaryrefslogtreecommitdiffstats
path: root/packages/mobile
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
parent2df08ed08c065e8b91bc8df0266bd4bcbb062be4 (diff)
downloadkarakeep-04572a8e5081b1e4871e273cde9dbaaa44c52fe0.tar.zst
structure: Create apps dir and copy tooling dir from t3-turbo repo
Diffstat (limited to 'packages/mobile')
-rw-r--r--packages/mobile/.eslintrc.js4
-rw-r--r--packages/mobile/.gitignore39
-rw-r--r--packages/mobile/.npmrc1
-rw-r--r--packages/mobile/app.json57
-rw-r--r--packages/mobile/app/+not-found.tsx6
-rw-r--r--packages/mobile/app/_layout.tsx53
-rw-r--r--packages/mobile/app/dashboard/(tabs)/_layout.tsx38
-rw-r--r--packages/mobile/app/dashboard/(tabs)/index.tsx31
-rw-r--r--packages/mobile/app/dashboard/(tabs)/lists.tsx67
-rw-r--r--packages/mobile/app/dashboard/(tabs)/search.tsx35
-rw-r--r--packages/mobile/app/dashboard/(tabs)/settings.tsx41
-rw-r--r--packages/mobile/app/dashboard/_layout.tsx38
-rw-r--r--packages/mobile/app/dashboard/add-link.tsx57
-rw-r--r--packages/mobile/app/dashboard/add-note.tsx53
-rw-r--r--packages/mobile/app/dashboard/archive.tsx11
-rw-r--r--packages/mobile/app/dashboard/favourites.tsx11
-rw-r--r--packages/mobile/app/dashboard/lists/[slug].tsx31
-rw-r--r--packages/mobile/app/error.tsx9
-rw-r--r--packages/mobile/app/index.tsx20
-rw-r--r--packages/mobile/app/sharing.tsx99
-rw-r--r--packages/mobile/app/signin.tsx101
-rw-r--r--packages/mobile/assets/blur.jpegbin178818 -> 0 bytes
-rw-r--r--packages/mobile/assets/icon.pngbin2362 -> 0 bytes
-rw-r--r--packages/mobile/assets/splash.pngbin117993 -> 0 bytes
-rw-r--r--packages/mobile/babel.config.js9
-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
-rw-r--r--packages/mobile/eas.json19
-rw-r--r--packages/mobile/globals.css80
-rw-r--r--packages/mobile/lib/last-shared-intent.ts15
-rw-r--r--packages/mobile/lib/providers.tsx54
-rw-r--r--packages/mobile/lib/session.ts20
-rw-r--r--packages/mobile/lib/settings.ts29
-rw-r--r--packages/mobile/lib/storage-state.ts51
-rw-r--r--packages/mobile/lib/trpc.ts4
-rw-r--r--packages/mobile/lib/utils.ts6
-rw-r--r--packages/mobile/metro.config.js8
-rw-r--r--packages/mobile/nativewind-env.d.ts1
-rw-r--r--packages/mobile/package.json53
-rw-r--r--packages/mobile/tailwind.config.js71
-rw-r--r--packages/mobile/tsconfig.json10
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
deleted file mode 100644
index 387ce697..00000000
--- a/packages/mobile/assets/blur.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/mobile/assets/icon.png b/packages/mobile/assets/icon.png
deleted file mode 100644
index 71ead90c..00000000
--- a/packages/mobile/assets/icon.png
+++ /dev/null
Binary files differ
diff --git a/packages/mobile/assets/splash.png b/packages/mobile/assets/splash.png
deleted file mode 100644
index 3759c518..00000000
--- a/packages/mobile/assets/splash.png
+++ /dev/null
Binary files differ
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": {
- "@/*": ["./*"]
- }
- }
-}