Initial commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
|
||||
import { BookMarked, Globe, Home, User } from "@tamagui/lucide-icons";
|
||||
import { Tabs } from "expo-router";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { Paragraph } from "tamagui";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
initialRouteName="articles"
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarShowLabel: true,
|
||||
tabBarActiveTintColor: "$accent5",
|
||||
tabBarHideOnKeyboard: true,
|
||||
tabBarStyle: {
|
||||
backgroundColor: colorScheme === "dark" ? "black" : "white",
|
||||
borderTopWidth: 0,
|
||||
paddingBottom: 5,
|
||||
paddingTop: 5,
|
||||
},
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
textTransform: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="articles"
|
||||
options={{
|
||||
href: "/(authed)/(tabs)/articles",
|
||||
tabBarLabel: ({ color }) => (
|
||||
<Paragraph size="$2" color={color}>
|
||||
Actualités
|
||||
</Paragraph>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="sources"
|
||||
options={{
|
||||
href: "/(authed)/(tabs)/sources",
|
||||
tabBarLabel: ({ color }) => (
|
||||
<Paragraph size="$2" color={color}>
|
||||
Sources
|
||||
</Paragraph>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <Globe size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="bookmarks"
|
||||
options={{
|
||||
href: "/(authed)/(tabs)/bookmarks",
|
||||
tabBarLabel: ({ color }) => (
|
||||
<Paragraph size="$2" color={color}>
|
||||
Signets
|
||||
</Paragraph>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <BookMarked size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="account"
|
||||
options={{
|
||||
href: "/(authed)/(tabs)/account",
|
||||
tabBarLabel: ({ color }) => (
|
||||
<Paragraph size="$2" color={color}>
|
||||
Profil
|
||||
</Paragraph>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function Layout() {
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ChevronRight, Settings } from "@tamagui/lucide-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Label, ListItem, ScrollView, Separator, YGroup } from "tamagui";
|
||||
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<ScreenView.Heading title="Profile" />
|
||||
|
||||
<ScrollView width="100%">
|
||||
<Label>Settings</Label>
|
||||
<YGroup alignSelf="center" bordered size="$4">
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
onPress={() => router.push("/account/settings")}
|
||||
icon={Settings}
|
||||
iconAfter={ChevronRight}
|
||||
title="Settings"
|
||||
/>
|
||||
</YGroup.Item>
|
||||
<Separator />
|
||||
</YGroup>
|
||||
</ScrollView>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { Button, YStack } from "tamagui";
|
||||
|
||||
import { useLogout } from "@/api/request/identity-and-access/login";
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
|
||||
export default function Index() {
|
||||
const authState = useAuth();
|
||||
const { mutate, isPending } = useLogout();
|
||||
|
||||
const handleLogout = async () => {
|
||||
mutate(undefined, {
|
||||
onSuccess: () => authState.logout(),
|
||||
onError: () => authState.logout(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<ScreenView.Heading title="Paramètres" />
|
||||
|
||||
<YStack width="100%">
|
||||
<Button
|
||||
disabled={isPending}
|
||||
onPress={handleLogout}
|
||||
theme={isPending ? "disabled" : "accent"}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{isPending ? <ActivityIndicator /> : "Déconnexion"}
|
||||
</Button>
|
||||
</YStack>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Bookmark, MoreVertical, Share } from "@tamagui/lucide-icons";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Button, H5, ScrollView, Separator, XStack, YStack } from "tamagui";
|
||||
|
||||
import { useArticleDetails } from "@/api/request/feed-management/article";
|
||||
import { Article } from "@/api/schema/feed-management/article";
|
||||
import { safeMessage } from "@/api/shared";
|
||||
import { useRelativeTime } from "@/hooks/use-relative-time";
|
||||
import { ArticleCategoryPill, ArticleCoverImage } from "@/ui/components/content/article";
|
||||
import { SourceReferencePill } from "@/ui/components/content/source";
|
||||
import { BackButton } from "@/ui/components/controls/BackButton";
|
||||
import { IconButton } from "@/ui/components/controls/IconButton";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { LoadingView } from "@/ui/components/LoadingView";
|
||||
import { Caption, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function ArticleDetails() {
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams();
|
||||
const { data, isLoading, error } = useArticleDetails(id as string);
|
||||
const article: Article | undefined = data ?? undefined;
|
||||
const relativeTime = useRelativeTime(article?.publishedAt);
|
||||
|
||||
const handleReadIntegrality = async () => {
|
||||
await WebBrowser.openBrowserAsync(article!.link);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Erreur",
|
||||
text2: safeMessage(error),
|
||||
});
|
||||
router.replace("/(authed)/(tabs)/articles");
|
||||
}
|
||||
|
||||
if (isLoading || article === undefined) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<ScreenView.Heading
|
||||
leadingAction={<BackButton onPress={() => router.dismissTo("/(authed)/(tabs)/articles")} />}
|
||||
trailingActions={
|
||||
<>
|
||||
<IconButton onPress={() => {}} icon={<Bookmark size="$1" />} />
|
||||
<IconButton onPress={() => {}} icon={<Share size="$1" />} />
|
||||
<IconButton onPress={() => {}} icon={<MoreVertical size="$1" />} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ScrollView>
|
||||
<YStack>
|
||||
{article.metadata?.image && (
|
||||
<ArticleCoverImage uri={article.metadata.image} width="100%" height={225} marginBottom="$4" />
|
||||
)}
|
||||
</YStack>
|
||||
<YStack gap="$4" backgroundColor="$background">
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
{article.categories.map((category, index) => (
|
||||
<ArticleCategoryPill key={index} category={category.toLowerCase()} />
|
||||
))}
|
||||
</XStack>
|
||||
<H5 fontWeight="bold" marginBottom="$1">
|
||||
{article.title}
|
||||
</H5>
|
||||
|
||||
<YStack gap="$2">
|
||||
<SourceReferencePill data={article.source} />
|
||||
<XStack height={20} alignItems="center">
|
||||
<Caption>{relativeTime}</Caption>
|
||||
<Separator alignSelf="stretch" vertical marginHorizontal={16} />
|
||||
<Caption>{article.readingTime} minutes de lecture</Caption>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<Text size="$3" marginTop="$2">
|
||||
{article.body.trim()}
|
||||
</Text>
|
||||
</YStack>
|
||||
<Button width="100%" onPress={handleReadIntegrality} theme="accent" fontWeight="bold">
|
||||
Consulter l'article
|
||||
</Button>
|
||||
</ScrollView>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function Layout() {
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
|
||||
import { ScrollView, YStack } from "tamagui";
|
||||
|
||||
import { useArticleOverviewList } from "@/api/request/feed-management/article";
|
||||
import { useSourceOverviewList } from "@/api/request/feed-management/source";
|
||||
import { ArticleOverview } from "@/api/schema/feed-management/article";
|
||||
import { SourceOverview } from "@/api/schema/feed-management/source";
|
||||
import { useFlattenedItems } from "@/hooks/use-flattened-items";
|
||||
import { ArticleList, ArticleSkeletonList } from "@/ui/components/content/article";
|
||||
import { SourceList, SourceSkeletonList } from "@/ui/components/content/source";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Heading } from "@/ui/components/typography";
|
||||
|
||||
export default function Index() {
|
||||
const { data: articles, isLoading: articlesLoading } = useArticleOverviewList({ limit: 10 });
|
||||
const { data: sources, isLoading: sourcesLoading } = useSourceOverviewList();
|
||||
const articleOverviews: ArticleOverview[] = useFlattenedItems(articles);
|
||||
const sourcesOverviews: SourceOverview[] = useFlattenedItems(sources);
|
||||
|
||||
return (
|
||||
<ScreenView paddingBottom={0}>
|
||||
<Heading>Actualités</Heading>
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 0 }}>
|
||||
<YStack gap="$4">
|
||||
<YStack gap="$2">
|
||||
<ScreenView.Section title="Tendances" forwardLink="/(authed)/(tabs)/articles/trending" />
|
||||
|
||||
{articlesLoading && <ArticleSkeletonList displayMode="card" horizontal={true} />}
|
||||
{!articlesLoading && (
|
||||
<ArticleList
|
||||
data={articleOverviews}
|
||||
refreshing={articlesLoading}
|
||||
displayMode="card"
|
||||
horizontal={true}
|
||||
/>
|
||||
)}
|
||||
</YStack>
|
||||
<YStack gap="$2">
|
||||
<ScreenView.Section title="Nos sources" forwardLink="/(authed)/(tabs)/sources" />
|
||||
|
||||
{sourcesLoading && <SourceSkeletonList horizontal={true} />}
|
||||
{!sourcesLoading && (
|
||||
<SourceList data={sourcesOverviews} refreshing={sourcesLoading} horizontal={true} />
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
import { useInfiniteArticleOverviewList } from "@/api/request/feed-management/article";
|
||||
import { TrendingArticle } from "@/api/schema/feed-management/article";
|
||||
import { useFlattenedItems } from "@/hooks/use-flattened-items";
|
||||
import { ArticleList, ArticleSkeletonList } from "@/ui/components/content/article";
|
||||
import { BackButton } from "@/ui/components/controls/BackButton";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
|
||||
export default function Trending() {
|
||||
const router = useRouter();
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteArticleOverviewList(
|
||||
{ limit: 20 }
|
||||
);
|
||||
const articles: TrendingArticle[] = useFlattenedItems(data);
|
||||
|
||||
return (
|
||||
<ScreenView paddingBottom={0}>
|
||||
<ScreenView.Heading
|
||||
leadingAction={<BackButton onPress={() => router.dismissTo("/(authed)/(tabs)/articles")} />}
|
||||
title="Actualités"
|
||||
/>
|
||||
|
||||
{isLoading && <ArticleSkeletonList displayMode="magazine" />}
|
||||
{!isLoading && (
|
||||
<ArticleList
|
||||
data={articles}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
refreshing={isLoading}
|
||||
onRefresh={refetch}
|
||||
infiniteScroll={true}
|
||||
/>
|
||||
)}
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { Paragraph } from "tamagui";
|
||||
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Heading } from "@/ui/components/typography";
|
||||
|
||||
export default function Details() {
|
||||
const { id } = useLocalSearchParams();
|
||||
// const { data, isLoading } = useBookmarkedArticlesList(id as string);
|
||||
// const articles: BookmarkedArticle[] = useFlattenedItems(data);
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<Heading>Bookmark Infos</Heading>
|
||||
<Paragraph>{id}</Paragraph>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function Layout() {
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
|
||||
import { Plus, Search } from "@tamagui/lucide-icons";
|
||||
import { YStack } from "tamagui";
|
||||
|
||||
import { useBookmarkList } from "@/api/request/feed-management/bookmark";
|
||||
import { Bookmark } from "@/api/schema/feed-management/bookmark";
|
||||
import { useFlattenedItems } from "@/hooks/use-flattened-items";
|
||||
import { BookmarkList } from "@/ui/components/content/bookmark";
|
||||
import { IconButton } from "@/ui/components/controls/IconButton";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { LoadingView } from "@/ui/components/LoadingView";
|
||||
|
||||
export default function Index() {
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useBookmarkList();
|
||||
const bookmarks: Bookmark[] = useFlattenedItems(data);
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<ScreenView.Heading
|
||||
title="Bookmarks"
|
||||
leadingAction={<IconButton onPress={() => {}} icon={<Plus size="$1" />} />}
|
||||
trailingActions={<IconButton onPress={() => {}} icon={<Search size="$1" />} />}
|
||||
/>
|
||||
|
||||
<YStack width="100%">
|
||||
{isLoading && <LoadingView />}
|
||||
{!isLoading && (
|
||||
<BookmarkList
|
||||
data={bookmarks}
|
||||
refreshing={isLoading}
|
||||
onRefresh={refetch}
|
||||
infiniteScroll={true}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
/>
|
||||
)}
|
||||
</YStack>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Heading, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function SourceDetails() {
|
||||
const { name } = useLocalSearchParams();
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<Heading>Source Details</Heading>
|
||||
<Text>{name}</Text>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function Layout() {
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useSourceOverviewList } from "@/api/request/feed-management/source";
|
||||
import { SourceOverview } from "@/api/schema/feed-management/source";
|
||||
import { useFlattenedItems } from "@/hooks/use-flattened-items";
|
||||
import { SourceList, SourceSkeletonList } from "@/ui/components/content/source";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
|
||||
export default function Sources() {
|
||||
const { data, isLoading } = useSourceOverviewList();
|
||||
const sources: SourceOverview[] = useFlattenedItems(data);
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<ScreenView.Heading title="Sources" />
|
||||
|
||||
{isLoading && <SourceSkeletonList horizontal={false} />}
|
||||
{!isLoading && <SourceList data={sources} horizontal={false} />}
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
|
||||
export default function AuthedLayout() {
|
||||
const auth = useAuth();
|
||||
|
||||
if (!auth.isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!auth.isLoggedIn) {
|
||||
return <Redirect href="/signin" />;
|
||||
}
|
||||
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
|
||||
export default function AuthedLayout() {
|
||||
const auth = useAuth();
|
||||
|
||||
if (!auth.isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
return <Redirect href="/(authed)/(tabs)/articles" />;
|
||||
}
|
||||
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
|
||||
import { joiResolver } from "@hookform/resolvers/joi";
|
||||
import { Link, useRouter } from "expo-router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { YStack } from "tamagui";
|
||||
|
||||
import { usePasswordForgotten } from "@/api/request/identity-and-access/password";
|
||||
import { RequestPasswordPayload, RequestPasswordPayloadSchema } from "@/api/schema/identity-and-access/password";
|
||||
import { ErrorResponse, safeMessage } from "@/api/shared";
|
||||
import { FormEmailInput } from "@/ui/components/controls/forms";
|
||||
import { SubmitButton } from "@/ui/components/controls/SubmitButton";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Heading, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function PasswordRequest() {
|
||||
const { mutate, isPending } = usePasswordForgotten();
|
||||
const router = useRouter();
|
||||
|
||||
const { control, handleSubmit, formState } = useForm<RequestPasswordPayload>({
|
||||
resolver: joiResolver(RequestPasswordPayloadSchema),
|
||||
});
|
||||
|
||||
const onSubmit = (data: RequestPasswordPayload) => {
|
||||
mutate(data, {
|
||||
onSuccess: () => {
|
||||
Toast.show({
|
||||
text1: "Succès",
|
||||
text2: "Un mail avec les instructions vous a été envoyé",
|
||||
type: "success",
|
||||
});
|
||||
router.push("/(unauthed)/signin");
|
||||
},
|
||||
onError: (error: ErrorResponse) => {
|
||||
Toast.show({
|
||||
text1: "Erreur de connexion",
|
||||
text2: safeMessage(error),
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<YStack flex={1} gap="$4" width="100%" justifyContent="flex-start">
|
||||
<YStack gap="$4">
|
||||
<Heading>Mot de passe oublié ?</Heading>
|
||||
<Text>
|
||||
Veuillez entrer votre adresse e-mail pour recevoir un lien de réinitialisation de mot de passe.
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
<FormEmailInput control={control} name="email" />
|
||||
|
||||
<Link href="/signin" asChild>
|
||||
<Text>Vous avez pas de compte ? Se connecter</Text>
|
||||
</Link>
|
||||
</YStack>
|
||||
<SubmitButton
|
||||
label="Réinitialiser le mot de passe"
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
isPending={isPending}
|
||||
isValid={formState.isValid}
|
||||
/>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
|
||||
import { joiResolver } from "@hookform/resolvers/joi";
|
||||
import { Link, useRouter } from "expo-router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { YStack } from "tamagui";
|
||||
|
||||
import { useLogin } from "@/api/request/identity-and-access/login";
|
||||
import { LoginPayload, LoginPayloadSchema, LoginResponse } from "@/api/schema/identity-and-access/login";
|
||||
import { ErrorResponse, safeMessage } from "@/api/shared";
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
import { FormEmailInput, FormPasswordInput } from "@/ui/components/controls/forms";
|
||||
import { SubmitButton } from "@/ui/components/controls/SubmitButton";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Caption, Heading, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function SignIn() {
|
||||
const { mutate, isPending } = useLogin();
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
router.replace("/(authed)/(tabs)/articles");
|
||||
}
|
||||
|
||||
const { control, handleSubmit, formState } = useForm<LoginPayload>({
|
||||
resolver: joiResolver(LoginPayloadSchema),
|
||||
});
|
||||
|
||||
const onSubmit = (data: LoginPayload) => {
|
||||
mutate(data, {
|
||||
onSuccess: async (data: LoginResponse) => {
|
||||
auth.login(data.token, data.refresh_token);
|
||||
Toast.show({ text1: "Connexion réussie", type: "success" });
|
||||
},
|
||||
onError: (error: ErrorResponse) => {
|
||||
Toast.show({
|
||||
text1: "Erreur de connexion",
|
||||
text2: safeMessage(error),
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<YStack flex={1} gap="$4" width="100%" justifyContent="flex-start">
|
||||
<YStack gap="$4">
|
||||
<Heading>Connexion</Heading>
|
||||
<Text>Bienvenue sur CongoNews, la plateforme d'actualités intelligente</Text>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<FormEmailInput control={control} name="username" />
|
||||
<YStack gap="$2">
|
||||
<FormPasswordInput control={control} name="password" />
|
||||
<Link href="/password-request" asChild>
|
||||
<Text color="$accent6"> Mot de passe oublié ?</Text>
|
||||
</Link>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
<Caption>
|
||||
En continuant, vous acceptez les conditions d'utilisation de CongoNews et reconnaissez avoir lu
|
||||
notre politique de confidentialité.
|
||||
</Caption>
|
||||
<Link href="/signup" asChild>
|
||||
<Text>Vous n'avez pas de compte ? Créer un compte</Text>
|
||||
</Link>
|
||||
</YStack>
|
||||
<SubmitButton
|
||||
label="Se connecter"
|
||||
isPending={isPending}
|
||||
isValid={formState.isValid}
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
/>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
|
||||
import { joiResolver } from "@hookform/resolvers/joi";
|
||||
import { User } from "@tamagui/lucide-icons";
|
||||
import { Link, useRouter } from "expo-router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { YStack } from "tamagui";
|
||||
|
||||
import { useRegister } from "@/api/request/identity-and-access/register";
|
||||
import { RegisterPayload, RegisterPayloadSchema } from "@/api/schema/identity-and-access/register";
|
||||
import { ErrorResponse, safeMessage } from "@/api/shared";
|
||||
import { FormEmailInput, FormPasswordInput, FormTextInput } from "@/ui/components/controls/forms";
|
||||
import { SubmitButton } from "@/ui/components/controls/SubmitButton";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Caption, Heading, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function SingUp() {
|
||||
const router = useRouter();
|
||||
const { mutate, isPending } = useRegister();
|
||||
|
||||
const { control, handleSubmit, formState } = useForm<RegisterPayload>({
|
||||
resolver: joiResolver(RegisterPayloadSchema),
|
||||
});
|
||||
|
||||
const onSubmit = (data: RegisterPayload) => {
|
||||
mutate(data, {
|
||||
onSuccess: () => {
|
||||
Toast.show({
|
||||
text1: "Félicitations !",
|
||||
text2: "les détails de votre compte vous ont été envoyés par e-mail.",
|
||||
type: "success",
|
||||
});
|
||||
router.replace("/(unauthed)/signin");
|
||||
},
|
||||
onError: (error: ErrorResponse) => {
|
||||
Toast.show({
|
||||
text1: "Erreur",
|
||||
text2: safeMessage(error),
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenView>
|
||||
<YStack flex={1} gap="$4" width="100%" justifyContent="flex-start">
|
||||
<YStack gap="$4">
|
||||
<Heading>Inscription</Heading>
|
||||
<Text>Rejoignez la communauté CongoNews et restez informé des dernières actualités</Text>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<FormTextInput
|
||||
control={control}
|
||||
name="name"
|
||||
leadingAdornment={User}
|
||||
label="Nom complet"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
<FormEmailInput control={control} name="email" />
|
||||
<FormPasswordInput control={control} name="password" />
|
||||
</YStack>
|
||||
<Caption>
|
||||
En continuant, vous acceptez les conditions d'utilisation de CongoNews et reconnaissez avoir lu
|
||||
notre politique de confidentialité.
|
||||
</Caption>
|
||||
<Link href="/signin">
|
||||
<Text>Vous avez un compte ? Connectez-vous</Text>
|
||||
</Link>
|
||||
</YStack>
|
||||
<SubmitButton
|
||||
label="Créer un compte"
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
isPending={isPending}
|
||||
isValid={formState.isValid}
|
||||
/>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Link, useRouter } from "expo-router";
|
||||
import { Button, YStack } from "tamagui";
|
||||
|
||||
import { AppIcon } from "@/ui/components/AppIcon";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Caption, Display, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function Welcome() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ScreenView justifyContent="center">
|
||||
<AppIcon width={120} height={120} />
|
||||
<YStack width="100%" gap="$6">
|
||||
<YStack gap="$3">
|
||||
<Display textAlign="center">Bienvenue sur CongoNews</Display>
|
||||
<Text textAlign="center" lineHeight="$1" marginTop="auto">
|
||||
La première plateforme d'actualités intelligente qui vous aide à rester informé sur
|
||||
congolaise et internationale.
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$4">
|
||||
<Button onPress={() => router.push("/signin")} theme="accent" fontWeight="bold">
|
||||
Se connecter
|
||||
</Button>
|
||||
<Link href="/signup" asChild>
|
||||
<Text textAlign="center">Ouvrir un compte</Text>
|
||||
</Link>
|
||||
</YStack>
|
||||
|
||||
<Caption textAlign="center">
|
||||
En continuant, vous acceptez les conditions d'utilisation de CongoNews et reconnaissez avoir lu
|
||||
notre politique de confidentialité.
|
||||
</Caption>
|
||||
</YStack>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { View, YStack } from "tamagui";
|
||||
|
||||
import { AppIcon } from "@/ui/components/AppIcon";
|
||||
import { ScreenView } from "@/ui/components/layout";
|
||||
import { Heading, Text } from "@/ui/components/typography";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<ScreenView>
|
||||
<Stack.Screen options={{ title: "Oops !" }} />
|
||||
<View flex={1} backgroundColor="$background" padding="$4">
|
||||
<YStack alignItems="center" justifyContent="center" flex={1} gap="$4">
|
||||
<AppIcon width={100} height={100} />
|
||||
<YStack width="100%" gap="$6" alignItems="center" paddingHorizontal="$4">
|
||||
<YStack>
|
||||
<Heading fontWeight="bold" lineHeight="$8" textAlign="center">
|
||||
Une erreur s'est produite
|
||||
</Heading>
|
||||
<Text textAlign="center" lineHeight="$1" marginTop="auto">
|
||||
Nous avons une difficulté à charger la page que vous recherchez.
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
<Link href="/(unauthed)/welcome">
|
||||
<Text>Recommencer</Text>
|
||||
</Link>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</View>
|
||||
</ScreenView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
import { Stack } from "expo-router";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Theme } from "tamagui";
|
||||
|
||||
import { RootProviders } from "@/providers/root-providers";
|
||||
|
||||
export { ErrorBoundary } from "expo-router";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
|
||||
sendDefaultPii: true,
|
||||
debug: __DEV__,
|
||||
tracesSampleRate: 1.0,
|
||||
tracePropagationTargets: [/.*?/],
|
||||
spotlight: __DEV__,
|
||||
});
|
||||
|
||||
function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<RootProviders>
|
||||
<Theme name={colorScheme || "dark"}>
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
<Toast topOffset={insets.top + 10} position="top" visibilityTime={6_000} />
|
||||
</Theme>
|
||||
</RootProviders>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sentry.wrap(RootLayout);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Redirect } from "expo-router";
|
||||
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
|
||||
export default function Index() {
|
||||
const auth = useAuth();
|
||||
|
||||
if (!auth.isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return auth.isLoggedIn ? <Redirect href="/(authed)/(tabs)/articles" /> : <Redirect href="/(unauthed)/welcome" />;
|
||||
}
|
||||
Reference in New Issue
Block a user