style(biome): using biome for format, lint, check
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
bun run check-types && bun run check && bun run lint:check
|
bun run typecheck && bun run lint
|
||||||
|
|||||||
Vendored
+8
-10
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
@@ -10,21 +9,20 @@
|
|||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"quickfix.biome": "explicit",
|
|
||||||
"source.organizeImports.biome": "explicit",
|
"source.organizeImports.biome": "explicit",
|
||||||
"source.fixAll": "explicit",
|
"source.fixAll.biome": "explicit"
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.sortMembers": "explicit"
|
|
||||||
},
|
},
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules": true
|
||||||
|
},
|
||||||
|
"terminal.integrated.localEchoStyle": "dim",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"typescript.preferences.autoImportFileExcludePatterns": [
|
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||||
"next/router.d.ts",
|
"next/router.d.ts",
|
||||||
"next/dist/client/router.d.ts"
|
"next/dist/client/router.d.ts"
|
||||||
],
|
],
|
||||||
"terminal.integrated.localEchoStyle": "dim",
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
"search.exclude": {
|
|
||||||
"**/node_modules": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1",
|
"ignore": ["node_modules", ".git"],
|
||||||
"name": "basango",
|
"name": "basango",
|
||||||
"type": "collection",
|
"type": "collection",
|
||||||
"ignore": [
|
"version": "1"
|
||||||
"node_modules",
|
|
||||||
".git"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,59 @@
|
|||||||
{
|
{
|
||||||
"type": "project",
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Basango\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"cweagans/composer-patches": true,
|
||||||
|
"php-http/discovery": true,
|
||||||
|
"symfony/flex": true,
|
||||||
|
"symfony/runtime": true
|
||||||
|
},
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"patches": {
|
||||||
|
"symfony/monolog-bundle": {
|
||||||
|
"support telegram topic in configuration": "./patches/monolog-telegram-configuration.patch",
|
||||||
|
"support telegram topic in extension": "./patches/monolog-telegram-extension.patch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": false,
|
||||||
|
"require": "7.2.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-php72": "*",
|
||||||
|
"symfony/polyfill-php73": "*",
|
||||||
|
"symfony/polyfill-php74": "*",
|
||||||
|
"symfony/polyfill-php80": "*",
|
||||||
|
"symfony/polyfill-php81": "*"
|
||||||
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.4",
|
|
||||||
"ext-ctype": "*",
|
|
||||||
"ext-dom": "*",
|
|
||||||
"ext-iconv": "*",
|
|
||||||
"cweagans/composer-patches": "^1.7.3",
|
"cweagans/composer-patches": "^1.7.3",
|
||||||
"doctrine/dbal": "^3.9.4",
|
"doctrine/dbal": "^3.9.4",
|
||||||
"doctrine/doctrine-bundle": "^2.13.2",
|
"doctrine/doctrine-bundle": "^2.13.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4.1",
|
"doctrine/doctrine-migrations-bundle": "^3.4.1",
|
||||||
"doctrine/orm": "^3.3.1",
|
"doctrine/orm": "^3.3.1",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
"geoip2/geoip2": "^3.1",
|
"geoip2/geoip2": "^3.1",
|
||||||
"gesdinet/jwt-refresh-token-bundle": "^1.4",
|
"gesdinet/jwt-refresh-token-bundle": "^1.4",
|
||||||
"knplabs/knp-paginator-bundle": "^6.7",
|
"knplabs/knp-paginator-bundle": "^6.7",
|
||||||
@@ -20,6 +61,7 @@
|
|||||||
"lexik/jwt-authentication-bundle": "^3.1",
|
"lexik/jwt-authentication-bundle": "^3.1",
|
||||||
"martin-georgiev/postgresql-for-doctrine": "^3.5",
|
"martin-georgiev/postgresql-for-doctrine": "^3.5",
|
||||||
"matomo/device-detector": "^6.4",
|
"matomo/device-detector": "^6.4",
|
||||||
|
"php": ">=8.4",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.1",
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
"sentry/sentry-symfony": "^5.2",
|
"sentry/sentry-symfony": "^5.2",
|
||||||
@@ -63,44 +105,11 @@
|
|||||||
"symplify/easy-coding-standard": "^12.1.13",
|
"symplify/easy-coding-standard": "^12.1.13",
|
||||||
"tomasvotruba/class-leak": "^1.2.7"
|
"tomasvotruba/class-leak": "^1.2.7"
|
||||||
},
|
},
|
||||||
"config": {
|
|
||||||
"allow-plugins": {
|
|
||||||
"php-http/discovery": true,
|
|
||||||
"symfony/flex": true,
|
|
||||||
"symfony/runtime": true,
|
|
||||||
"cweagans/composer-patches": true
|
|
||||||
},
|
|
||||||
"sort-packages": true
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Basango\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload-dev": {
|
|
||||||
"psr-4": {
|
|
||||||
"Tests\\": "tests/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"symfony/polyfill-ctype": "*",
|
|
||||||
"symfony/polyfill-iconv": "*",
|
|
||||||
"symfony/polyfill-php72": "*",
|
|
||||||
"symfony/polyfill-php73": "*",
|
|
||||||
"symfony/polyfill-php74": "*",
|
|
||||||
"symfony/polyfill-php80": "*",
|
|
||||||
"symfony/polyfill-php81": "*"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"auto-scripts": {
|
"app:behat": [
|
||||||
"cache:clear": "symfony-cmd",
|
"APP_ENV=test bin/console doctrine:database:create",
|
||||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
"APP_ENV=test bin/console doctrine:migration:migrate --no-interaction --allow-no-migration --all-or-nothing",
|
||||||
},
|
"APP_ENV=test ./vendor/bin/behat --format=progress --no-interaction"
|
||||||
"post-install-cmd": [
|
|
||||||
"@auto-scripts"
|
|
||||||
],
|
|
||||||
"post-update-cmd": [
|
|
||||||
"@auto-scripts"
|
|
||||||
],
|
],
|
||||||
"app:cs": [
|
"app:cs": [
|
||||||
"./vendor/bin/ecs check",
|
"./vendor/bin/ecs check",
|
||||||
@@ -110,32 +119,17 @@
|
|||||||
"./vendor/bin/phpstan analyse --memory-limit=-1 --configuration=phpstan.dist.neon",
|
"./vendor/bin/phpstan analyse --memory-limit=-1 --configuration=phpstan.dist.neon",
|
||||||
"./vendor/bin/rector --dry-run"
|
"./vendor/bin/rector --dry-run"
|
||||||
],
|
],
|
||||||
"app:tests": [
|
|
||||||
"APP_ENV=test ./vendor/bin/phpunit"
|
|
||||||
],
|
|
||||||
"app:behat": [
|
|
||||||
"APP_ENV=test bin/console doctrine:database:create",
|
|
||||||
"APP_ENV=test bin/console doctrine:migration:migrate --no-interaction --allow-no-migration --all-or-nothing",
|
|
||||||
"APP_ENV=test ./vendor/bin/behat --format=progress --no-interaction"
|
|
||||||
],
|
|
||||||
"app:env": [
|
"app:env": [
|
||||||
"APP_RUNTIME_ENV=prod bin/console secrets:decrypt-to-local --force",
|
"APP_RUNTIME_ENV=prod bin/console secrets:decrypt-to-local --force",
|
||||||
"bin/console dotenv:dump prod"
|
"bin/console dotenv:dump prod"
|
||||||
]
|
],
|
||||||
|
"app:tests": ["APP_ENV=test ./vendor/bin/phpunit"],
|
||||||
|
"auto-scripts": {
|
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||||
|
"cache:clear": "symfony-cmd"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"post-install-cmd": ["@auto-scripts"],
|
||||||
"symfony/symfony": "*"
|
"post-update-cmd": ["@auto-scripts"]
|
||||||
},
|
},
|
||||||
"extra": {
|
"type": "project"
|
||||||
"symfony": {
|
|
||||||
"allow-contrib": false,
|
|
||||||
"require": "7.2.*"
|
|
||||||
},
|
|
||||||
"patches": {
|
|
||||||
"symfony/monolog-bundle": {
|
|
||||||
"support telegram topic in configuration": "./patches/monolog-telegram-configuration.patch",
|
|
||||||
"support telegram topic in extension": "./patches/monolog-telegram-extension.patch"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@basango/crawler",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@basango/logger": "workspace:*",
|
"@basango/logger": "workspace:*",
|
||||||
"@devscast/config": "^1.0.3",
|
"@devscast/config": "^1.0.3",
|
||||||
@@ -16,16 +14,18 @@
|
|||||||
"@types/turndown": "^5.0.6",
|
"@types/turndown": "^5.0.6",
|
||||||
"vitest": "^4.0.7"
|
"vitest": "^4.0.7"
|
||||||
},
|
},
|
||||||
|
"name": "@basango/crawler",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"crawler:async": "bun run src/scripts/queue.ts",
|
"crawler:async": "bun run src/scripts/queue.ts",
|
||||||
"crawler:sync": "bun run src/scripts/crawl.ts",
|
"crawler:sync": "bun run src/scripts/crawl.ts",
|
||||||
"crawler:worker": "bun run src/scripts/worker.ts",
|
"crawler:worker": "bun run src/scripts/worker.ts",
|
||||||
"clean": "rm -rf .turbo node_modules",
|
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"lint:fix": "biome check --write .",
|
"lint:fix": "biome check --write .",
|
||||||
"typecheck": "tsc --noEmit",
|
"test": "vitest --run",
|
||||||
"test": "vitest --run"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extends": "@basango/typescript-config/base.json",
|
"extends": "@basango/tsconfig/base.json",
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"references": []
|
"references": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
variable: "--font-geist-sans",
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
variable: "--font-geist-mono",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
|
||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
|
title: "Create Next App",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -24,11 +24,7 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -5,12 +5,12 @@ export default function Home() {
|
|||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||||
<Image
|
<Image
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
alt="Next.js logo"
|
||||||
width={100}
|
className="dark:invert"
|
||||||
height={20}
|
height={20}
|
||||||
priority
|
priority
|
||||||
|
src="/next.svg"
|
||||||
|
width={100}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||||
@@ -19,15 +19,15 @@ export default function Home() {
|
|||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
Looking for a starting point or more instructions? Head over to{" "}
|
||||||
<a
|
<a
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
>
|
>
|
||||||
Templates
|
Templates
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
or the{" "}
|
or the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
>
|
>
|
||||||
Learning
|
Learning
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -38,23 +38,23 @@ export default function Home() {
|
|||||||
<a
|
<a
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
alt="Vercel logomark"
|
||||||
width={16}
|
className="dark:invert"
|
||||||
height={16}
|
height={16}
|
||||||
|
src="/vercel.svg"
|
||||||
|
width={16}
|
||||||
/>
|
/>
|
||||||
Deploy Now
|
Deploy Now
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
Documentation
|
Documentation
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
|
||||||
"rsc": true,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "../../packages/ui/src/styles/globals.css",
|
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"hooks": "@/hooks",
|
"hooks": "@/hooks",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"utils": "@basango/ui/lib/utils",
|
"ui": "@basango/ui/components",
|
||||||
"ui": "@basango/ui/components"
|
"utils": "@basango/ui/lib/utils"
|
||||||
}
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rsc": true,
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"config": "",
|
||||||
|
"css": "../../packages/ui/src/styles/globals.css",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"tsx": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
transpilePackages: ["@basango/ui"],
|
transpilePackages: ["@basango/ui"],
|
||||||
}
|
};
|
||||||
|
|
||||||
export default nextConfig
|
export default nextConfig;
|
||||||
|
|||||||
+13
-13
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@basango/dashboard",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "eslint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"next": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:"
|
||||||
"next": "catalog:"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "catalog:",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"@tailwindcss/postcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tailwindcss": "^4"
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"name": "@basango/dashboard",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"dev": "next dev",
|
||||||
|
"lint": "eslint",
|
||||||
|
"start": "next start"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "@basango/typescript-config/nextjs.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -12,12 +11,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"exclude": ["node_modules"],
|
||||||
"next-env.d.ts",
|
"extends": "@basango/tsconfig/nextjs.json",
|
||||||
"next.config.ts",
|
"include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-34
@@ -1,40 +1,43 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "basango",
|
|
||||||
"slug": "basango",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"icon": "./src/assets/images/logo.png",
|
|
||||||
"scheme": "basango",
|
|
||||||
"userInterfaceStyle": "automatic",
|
|
||||||
"newArchEnabled": true,
|
|
||||||
"githubUrl": "https://github.com/bernard-ng/basango",
|
|
||||||
"ios": {
|
|
||||||
"supportsTablet": true,
|
|
||||||
"bundleIdentifier": "dev.ngandu.basango"
|
|
||||||
},
|
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./src/assets/images/logo.png",
|
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
|
"foregroundImage": "./src/assets/images/logo.png",
|
||||||
"package": "dev.ngandu.basango"
|
"package": "dev.ngandu.basango"
|
||||||
},
|
},
|
||||||
"package": "dev.ngandu.basango"
|
"package": "dev.ngandu.basango"
|
||||||
},
|
},
|
||||||
"web": {
|
"experiments": {
|
||||||
"bundler": "metro",
|
"typedRoutes": true
|
||||||
"output": "static",
|
|
||||||
"favicon": "./src/assets/images/logo.png"
|
|
||||||
},
|
},
|
||||||
|
"extra": {
|
||||||
|
"eas": {
|
||||||
|
"projectId": "57281e7a-46e3-4aac-8715-5165fa0bf560"
|
||||||
|
},
|
||||||
|
"router": {
|
||||||
|
"origin": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"githubUrl": "https://github.com/bernard-ng/basango",
|
||||||
|
"icon": "./src/assets/images/logo.png",
|
||||||
|
"ios": {
|
||||||
|
"bundleIdentifier": "dev.ngandu.basango",
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"name": "basango",
|
||||||
|
"newArchEnabled": true,
|
||||||
|
"orientation": "portrait",
|
||||||
|
"owner": "bernard-ng",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
"image": "./src/assets/images/logo.png",
|
"image": "./src/assets/images/logo.png",
|
||||||
"imageWidth": 200,
|
"imageWidth": 200,
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain"
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
@@ -42,23 +45,20 @@
|
|||||||
[
|
[
|
||||||
"@sentry/react-native/expo",
|
"@sentry/react-native/expo",
|
||||||
{
|
{
|
||||||
"url": "https://glitchtip.devscast.tech/",
|
"organization": "devscast-software",
|
||||||
"project": "basango",
|
"project": "basango",
|
||||||
"organization": "devscast-software"
|
"url": "https://glitchtip.devscast.tech/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"scheme": "basango",
|
||||||
"typedRoutes": true
|
"slug": "basango",
|
||||||
},
|
"userInterfaceStyle": "automatic",
|
||||||
"extra": {
|
"version": "1.0.0",
|
||||||
"router": {
|
"web": {
|
||||||
"origin": false
|
"bundler": "metro",
|
||||||
},
|
"favicon": "./src/assets/images/logo.png",
|
||||||
"eas": {
|
"output": "static"
|
||||||
"projectId": "57281e7a-46e3-4aac-8715-5165fa0bf560"
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"owner": "bernard-ng"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = function (api) {
|
module.exports = (api) => {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: [["babel-preset-expo", { jsxRuntime: "automatic" }]],
|
|
||||||
plugins: ["react-native-reanimated/plugin"],
|
plugins: ["react-native-reanimated/plugin"],
|
||||||
|
presets: [["babel-preset-expo", { jsxRuntime: "automatic" }]],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@commitlint/config-conventional"],
|
extends: ["@commitlint/config-conventional"],
|
||||||
rules: {
|
rules: {
|
||||||
|
"subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
|
||||||
"type-enum": [
|
"type-enum": [
|
||||||
2,
|
2,
|
||||||
"always",
|
"always",
|
||||||
@@ -18,6 +19,5 @@ module.exports = {
|
|||||||
"revert", // Reverts a previous commit
|
"revert", // Reverts a previous commit
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"cli": {
|
|
||||||
"version": ">= 16.3.1",
|
|
||||||
"appVersionSource": "remote"
|
|
||||||
},
|
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
"developmentClient": true,
|
"developmentClient": true,
|
||||||
@@ -15,6 +11,10 @@
|
|||||||
"autoIncrement": true
|
"autoIncrement": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cli": {
|
||||||
|
"appVersionSource": "remote",
|
||||||
|
"version": ">= 16.3.1"
|
||||||
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
"production": {}
|
"production": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
const { defineConfig } = require("eslint/config");
|
|
||||||
const expoConfig = require("eslint-config-expo/flat");
|
|
||||||
const prettierPlugin = require("eslint-plugin-prettier");
|
|
||||||
const unusedImportsPlugin = require("eslint-plugin-unused-imports");
|
|
||||||
|
|
||||||
module.exports = defineConfig([
|
|
||||||
expoConfig,
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
prettier: prettierPlugin,
|
|
||||||
"unused-imports": unusedImportsPlugin,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"import/default": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"import/named": "off",
|
|
||||||
"import/namespace": "error",
|
|
||||||
"import/export": "error",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
|
||||||
"unused-imports/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
vars: "all",
|
|
||||||
varsIgnorePattern: "^_",
|
|
||||||
args: "after-used",
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
groups: ["builtin", "external", "internal"],
|
|
||||||
pathGroups: [
|
|
||||||
{
|
|
||||||
pattern: "react",
|
|
||||||
group: "external",
|
|
||||||
position: "before",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pathGroupsExcludedImportTypes: ["react"],
|
|
||||||
"newlines-between": "always",
|
|
||||||
alphabetize: {
|
|
||||||
order: "asc",
|
|
||||||
caseInsensitive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: ["dist/*"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"expo.jsEngine": "hermes",
|
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
|
||||||
"newArchEnabled": "true",
|
|
||||||
"apple.extraPods": "[]",
|
|
||||||
"apple.ccacheEnabled": "false",
|
"apple.ccacheEnabled": "false",
|
||||||
"apple.privacyManifestAggregationEnabled": "true"
|
"apple.extraPods": "[]",
|
||||||
|
"apple.privacyManifestAggregationEnabled": "true",
|
||||||
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
|
"expo.jsEngine": "hermes",
|
||||||
|
"newArchEnabled": "true"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info": {
|
||||||
"version": 1,
|
"author": "expo",
|
||||||
"author": "expo"
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"version" : 1,
|
"author": "expo",
|
||||||
"author" : "expo"
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2,19 +2,19 @@
|
|||||||
"colors": [
|
"colors": [
|
||||||
{
|
{
|
||||||
"color": {
|
"color": {
|
||||||
|
"color-space": "srgb",
|
||||||
"components": {
|
"components": {
|
||||||
"alpha": "1.000",
|
"alpha": "1.000",
|
||||||
"blue": "1.00000000000000",
|
"blue": "1.00000000000000",
|
||||||
"green": "1.00000000000000",
|
"green": "1.00000000000000",
|
||||||
"red": "1.00000000000000"
|
"red": "1.00000000000000"
|
||||||
},
|
}
|
||||||
"color-space": "srgb"
|
|
||||||
},
|
},
|
||||||
"idiom": "universal"
|
"idiom": "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info": {
|
||||||
"version": 1,
|
"author": "expo",
|
||||||
"author": "expo"
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images": [
|
||||||
{
|
{
|
||||||
"idiom": "universal",
|
|
||||||
"filename": "image.png",
|
"filename": "image.png",
|
||||||
|
"idiom": "universal",
|
||||||
"scale": "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom": "universal",
|
|
||||||
"filename": "image@2x.png",
|
"filename": "image@2x.png",
|
||||||
|
"idiom": "universal",
|
||||||
"scale": "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom": "universal",
|
|
||||||
"filename": "image@3x.png",
|
"filename": "image@3x.png",
|
||||||
|
"idiom": "universal",
|
||||||
"scale": "3x"
|
"scale": "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info": {
|
||||||
"version": 1,
|
"author": "expo",
|
||||||
"author": "expo"
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,49 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "drc-news",
|
|
||||||
"main": "expo-router/entry",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"scripts": {
|
|
||||||
"start": "expo start",
|
|
||||||
"android": "expo run:android",
|
|
||||||
"ios": "expo run:ios",
|
|
||||||
"web": "expo start --web",
|
|
||||||
"test": "jest --watchAll",
|
|
||||||
"============= EAS BUILD =============": "",
|
|
||||||
"build:ios:dev": "eas build --profile development --platform ios",
|
|
||||||
"build:ios:sim": "eas build --profile dev-sim --platform ios",
|
|
||||||
"build:ios:prev": "eas build --profile preview --platform ios",
|
|
||||||
"build:ios:e2e": "eas build --profile ios-e2e --platform ios",
|
|
||||||
"build:android:dev": "eas build --profile development --platform android",
|
|
||||||
"build:android:sim": "eas build --profile dev-sim --platform android",
|
|
||||||
"build:android:prev": "eas build --profile preview --platform android",
|
|
||||||
"build:android:e2e": "eas build --profile android-e2e --platform android",
|
|
||||||
"build:android:prod": "eas build --profile production --platform android",
|
|
||||||
"build:ios:prod": "eas build --profile production --platform ios",
|
|
||||||
"===================== EAS SUBMIT =====================": "",
|
|
||||||
"eas:android:submit": "eas submit -p android --profile production",
|
|
||||||
"eas:ios:submit": "eas submit -p ios --profile production",
|
|
||||||
"=========== CODE STYLE ============": "",
|
|
||||||
"check-types": "tsc --noEmit",
|
|
||||||
"check": "prettier src --check",
|
|
||||||
"format": "prettier src --write",
|
|
||||||
"lint:check": "eslint src --debug",
|
|
||||||
"lint:fix": "eslint src --fix",
|
|
||||||
"============= HUSKY =============": "",
|
|
||||||
"prepare": "husky",
|
|
||||||
"commit": "cz",
|
|
||||||
"============= MISCELLANEOUS =============": "",
|
|
||||||
"delete:dstore": "find -name '.DS_Store' -type f -delete"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.ts": [
|
|
||||||
"prettier --write",
|
|
||||||
"eslint --fix"
|
|
||||||
],
|
|
||||||
"*.tsx": [
|
|
||||||
"prettier --write",
|
|
||||||
"eslint --fix"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"@commitlint/config-conventional"
|
"@commitlint/config-conventional"
|
||||||
@@ -54,12 +9,6 @@
|
|||||||
"path": "cz-conventional-changelog"
|
"path": "cz-conventional-changelog"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"preset": "jest-expo"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"globals": "14.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo-google-fonts/inter": "^0.3.0",
|
"@expo-google-fonts/inter": "^0.3.0",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
@@ -132,5 +81,56 @@
|
|||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"private": true
|
"jest": {
|
||||||
|
"preset": "jest-expo"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.ts": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.tsx": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"main": "expo-router/entry",
|
||||||
|
"name": "drc-news",
|
||||||
|
"overrides": {
|
||||||
|
"globals": "14.0.0"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"=========== CODE STYLE ============": "",
|
||||||
|
"============= EAS BUILD =============": "",
|
||||||
|
"============= HUSKY =============": "",
|
||||||
|
"============= MISCELLANEOUS =============": "",
|
||||||
|
"===================== EAS SUBMIT =====================": "",
|
||||||
|
"android": "expo run:android",
|
||||||
|
"build:android:dev": "eas build --profile development --platform android",
|
||||||
|
"build:android:e2e": "eas build --profile android-e2e --platform android",
|
||||||
|
"build:android:prev": "eas build --profile preview --platform android",
|
||||||
|
"build:android:prod": "eas build --profile production --platform android",
|
||||||
|
"build:android:sim": "eas build --profile dev-sim --platform android",
|
||||||
|
"build:ios:dev": "eas build --profile development --platform ios",
|
||||||
|
"build:ios:e2e": "eas build --profile ios-e2e --platform ios",
|
||||||
|
"build:ios:prev": "eas build --profile preview --platform ios",
|
||||||
|
"build:ios:prod": "eas build --profile production --platform ios",
|
||||||
|
"build:ios:sim": "eas build --profile dev-sim --platform ios",
|
||||||
|
"check": "prettier src --check",
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
|
"commit": "cz",
|
||||||
|
"delete:dstore": "find -name '.DS_Store' -type f -delete",
|
||||||
|
"eas:android:submit": "eas submit -p android --profile production",
|
||||||
|
"eas:ios:submit": "eas submit -p ios --profile production",
|
||||||
|
"format": "prettier src --write",
|
||||||
|
"ios": "expo run:ios",
|
||||||
|
"lint:check": "eslint src --debug",
|
||||||
|
"lint:fix": "eslint src --fix",
|
||||||
|
"prepare": "husky",
|
||||||
|
"start": "expo start",
|
||||||
|
"test": "jest --watchAll",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const endpoint = process.env.EXPO_PUBLIC_API_URL!;
|
|||||||
const client: AxiosInstance = axios.create({
|
const client: AxiosInstance = axios.create({
|
||||||
baseURL: endpoint,
|
baseURL: endpoint,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -16,21 +16,21 @@ let isAuthTokenRefreshing = false;
|
|||||||
let failedRequestsQueue: ((token: string) => void)[] = [];
|
let failedRequestsQueue: ((token: string) => void)[] = [];
|
||||||
|
|
||||||
const processFailedRequestsQueue = (token: string) => {
|
const processFailedRequestsQueue = (token: string) => {
|
||||||
failedRequestsQueue.forEach(callback => callback(token));
|
failedRequestsQueue.forEach((callback) => callback(token));
|
||||||
failedRequestsQueue = [];
|
failedRequestsQueue = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wait for 120 seconds before timing out
|
// Wait for 120 seconds before timing out
|
||||||
axios.interceptors.request.use(config => {
|
axios.interceptors.request.use((config) => {
|
||||||
config.timeout = 120_000;
|
config.timeout = 120_000;
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the Authorization header to all requests
|
// Add the Authorization header to all requests
|
||||||
client.interceptors.request.use(async config => {
|
client.interceptors.request.use(async (config) => {
|
||||||
const token = await getAccessToken();
|
const token = await getAccessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers["Authorization"] = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
@@ -38,8 +38,8 @@ client.interceptors.request.use(async config => {
|
|||||||
|
|
||||||
// Handle 401 errors and refresh the token
|
// Handle 401 errors and refresh the token
|
||||||
client.interceptors.response.use(
|
client.interceptors.response.use(
|
||||||
response => response,
|
(response) => response,
|
||||||
async error => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
|
|
||||||
@@ -47,9 +47,9 @@ client.interceptors.response.use(
|
|||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
|
|
||||||
if (isAuthTokenRefreshing) {
|
if (isAuthTokenRefreshing) {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
failedRequestsQueue.push((token: string) => {
|
failedRequestsQueue.push((token: string) => {
|
||||||
originalRequest.headers["Authorization"] = `Bearer ${token}`;
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||||
resolve(client(originalRequest));
|
resolve(client(originalRequest));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -72,7 +72,7 @@ client.interceptors.response.use(
|
|||||||
await setTokens(updatedToken, refreshToken);
|
await setTokens(updatedToken, refreshToken);
|
||||||
processFailedRequestsQueue(updatedToken);
|
processFailedRequestsQueue(updatedToken);
|
||||||
|
|
||||||
originalRequest.headers["Authorization"] = `Bearer ${updatedToken}`;
|
originalRequest.headers.Authorization = `Bearer ${updatedToken}`;
|
||||||
return client(originalRequest);
|
return client(originalRequest);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await clearTokens();
|
await clearTokens();
|
||||||
@@ -83,34 +83,34 @@ client.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// Log HTTP requests and responses
|
// Log HTTP requests and responses
|
||||||
client.interceptors.request.use(
|
client.interceptors.request.use(
|
||||||
async config => {
|
async (config) => {
|
||||||
console.log("HTTP REQUEST", {
|
console.log("HTTP REQUEST", {
|
||||||
baseURL: config.baseURL,
|
baseURL: config.baseURL,
|
||||||
url: config.url,
|
|
||||||
data: config.data,
|
data: config.data,
|
||||||
|
url: config.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
error => console.log(JSON.stringify(error))
|
(error) => console.log(JSON.stringify(error)),
|
||||||
);
|
);
|
||||||
|
|
||||||
client.interceptors.response.use(
|
client.interceptors.response.use(
|
||||||
response => {
|
(response) => {
|
||||||
console.log("HTTP RESPONSE", {
|
console.log("HTTP RESPONSE", {
|
||||||
stats: response.status,
|
|
||||||
data: response.data,
|
data: response.data,
|
||||||
|
stats: response.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
error => console.log(JSON.stringify(error))
|
(error) => console.log(JSON.stringify(error)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export const endpoint = {
|
|||||||
getArticleCommentList: (articleId: string) => `/feed/articles/${articleId}/comments`,
|
getArticleCommentList: (articleId: string) => `/feed/articles/${articleId}/comments`,
|
||||||
getArticleDetails: (articleId: string) => `/feed/articles/${articleId}`,
|
getArticleDetails: (articleId: string) => `/feed/articles/${articleId}`,
|
||||||
getArticleOverviewList: `/feed/articles`,
|
getArticleOverviewList: `/feed/articles`,
|
||||||
getBookmarkList: `/feed/bookmarks`,
|
|
||||||
getBookmarkedArticlesList: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}/articles`,
|
getBookmarkedArticlesList: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}/articles`,
|
||||||
|
getBookmarkList: `/feed/bookmarks`,
|
||||||
getSourceArticleOverviewList: (sourceId: string) => `/feed/sources/${sourceId}/articles`,
|
getSourceArticleOverviewList: (sourceId: string) => `/feed/sources/${sourceId}/articles`,
|
||||||
getSourceDetails: (sourceId: string) => `/feed/sources/${sourceId}`,
|
getSourceDetails: (sourceId: string) => `/feed/sources/${sourceId}`,
|
||||||
getSourceOverviewList: `/feed/sources`,
|
getSourceOverviewList: `/feed/sources`,
|
||||||
@@ -20,12 +20,12 @@ export const endpoint = {
|
|||||||
updateBookmark: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}`,
|
updateBookmark: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}`,
|
||||||
},
|
},
|
||||||
identityAndAccess: {
|
identityAndAccess: {
|
||||||
|
confirmAccount: (token: string) => `/account/confirm/${token}`,
|
||||||
|
getUserProfile: "/me",
|
||||||
login: "/login_check",
|
login: "/login_check",
|
||||||
logout: "/token/invalidate",
|
logout: "/token/invalidate",
|
||||||
register: "/register",
|
register: "/register",
|
||||||
getUserProfile: "/me",
|
|
||||||
requestPassword: "/password/request",
|
requestPassword: "/password/request",
|
||||||
confirmAccount: (token: string) => `/account/confirm/${token}`,
|
|
||||||
resetPassword: (token: string) => `/password/reset/${token}`,
|
resetPassword: (token: string) => `/password/reset/${token}`,
|
||||||
unlockAccount: (token: string) => `/account/unlock/${token}`,
|
unlockAccount: (token: string) => `/account/unlock/${token}`,
|
||||||
updatePassword: "/password/update",
|
updatePassword: "/password/update",
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { endpoint } from "@/api/endpoint";
|
import { endpoint } from "@/api/endpoint";
|
||||||
import { Article, ArticleOverview, TrendingArticle } from "@/api/schema/feed-management/article";
|
import { Article, ArticleOverview, TrendingArticle } from "@/api/schema/feed-management/article";
|
||||||
import { ArticleFilters, useGetQuery, usePaginatedInfiniteQuery, usePaginatedQuery } from "@/api/shared";
|
import {
|
||||||
|
ArticleFilters,
|
||||||
|
useGetQuery,
|
||||||
|
usePaginatedInfiniteQuery,
|
||||||
|
usePaginatedQuery,
|
||||||
|
} from "@/api/shared";
|
||||||
|
|
||||||
export const useArticleTrendingList = (filters: ArticleFilters = {}) => {
|
export const useArticleTrendingList = (filters: ArticleFilters = {}) => {
|
||||||
return usePaginatedQuery<TrendingArticle>("/feed/trending", filters);
|
return usePaginatedQuery<TrendingArticle>("/feed/trending", filters);
|
||||||
@@ -11,9 +16,15 @@ export const useArticleDetails = (articleId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useArticleOverviewList = (filters: ArticleFilters = {}) => {
|
export const useArticleOverviewList = (filters: ArticleFilters = {}) => {
|
||||||
return usePaginatedQuery<ArticleOverview>(endpoint.feedManagement.getArticleOverviewList, filters);
|
return usePaginatedQuery<ArticleOverview>(
|
||||||
|
endpoint.feedManagement.getArticleOverviewList,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfiniteArticleOverviewList = (filters: ArticleFilters = {}) => {
|
export const useInfiniteArticleOverviewList = (filters: ArticleFilters = {}) => {
|
||||||
return usePaginatedInfiniteQuery<ArticleOverview>(endpoint.feedManagement.getArticleOverviewList, filters);
|
return usePaginatedInfiniteQuery<ArticleOverview>(
|
||||||
|
endpoint.feedManagement.getArticleOverviewList,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { endpoint } from "@/api/endpoint";
|
import { endpoint } from "@/api/endpoint";
|
||||||
import { Bookmark, BookmarkedArticle, BookmarkPayload } from "@/api/schema/feed-management/bookmark";
|
import {
|
||||||
import { ArticleFilters, useDeleteQuery, usePaginatedInfiniteQuery, usePostQuery, usePutQuery } from "@/api/shared";
|
Bookmark,
|
||||||
|
BookmarkedArticle,
|
||||||
|
BookmarkPayload,
|
||||||
|
} from "@/api/schema/feed-management/bookmark";
|
||||||
|
import {
|
||||||
|
ArticleFilters,
|
||||||
|
useDeleteQuery,
|
||||||
|
usePaginatedInfiniteQuery,
|
||||||
|
usePostQuery,
|
||||||
|
usePutQuery,
|
||||||
|
} from "@/api/shared";
|
||||||
|
|
||||||
export const useCreateBookmark = () => {
|
export const useCreateBookmark = () => {
|
||||||
return usePostQuery<BookmarkPayload>(endpoint.feedManagement.createBookmark);
|
return usePostQuery<BookmarkPayload>(endpoint.feedManagement.createBookmark);
|
||||||
@@ -29,6 +39,6 @@ export const useBookmarkList = (filters: ArticleFilters = {}) => {
|
|||||||
export const useBookmarkedArticlesList = (bookmarkId: string, filters: ArticleFilters = {}) => {
|
export const useBookmarkedArticlesList = (bookmarkId: string, filters: ArticleFilters = {}) => {
|
||||||
return usePaginatedInfiniteQuery<BookmarkedArticle>(
|
return usePaginatedInfiniteQuery<BookmarkedArticle>(
|
||||||
endpoint.feedManagement.getBookmarkedArticlesList(bookmarkId),
|
endpoint.feedManagement.getBookmarkedArticlesList(bookmarkId),
|
||||||
filters
|
filters,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { Comment, CommentPayload } from "@/api/schema/feed-management/comment";
|
|||||||
import { useDeleteQuery, usePaginatedInfiniteQuery, usePostQuery } from "@/api/shared";
|
import { useDeleteQuery, usePaginatedInfiniteQuery, usePostQuery } from "@/api/shared";
|
||||||
|
|
||||||
export const useArticleCommentList = (articleId: string) => {
|
export const useArticleCommentList = (articleId: string) => {
|
||||||
return usePaginatedInfiniteQuery<Comment>(endpoint.feedManagement.getArticleCommentList(articleId));
|
return usePaginatedInfiniteQuery<Comment>(
|
||||||
|
endpoint.feedManagement.getArticleCommentList(articleId),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAddCommentToArticle = (articleId: string) => {
|
export const useAddCommentToArticle = (articleId: string) => {
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
import { endpoint } from "@/api/endpoint";
|
import { endpoint } from "@/api/endpoint";
|
||||||
import { ArticleOverview } from "@/api/schema/feed-management/article";
|
import { ArticleOverview } from "@/api/schema/feed-management/article";
|
||||||
import { SourceDetails, SourceOverview } from "@/api/schema/feed-management/source";
|
import { SourceDetails, SourceOverview } from "@/api/schema/feed-management/source";
|
||||||
import { ArticleFilters, useDeleteQuery, useGetQuery, usePaginatedInfiniteQuery, usePostQuery } from "@/api/shared";
|
import {
|
||||||
|
ArticleFilters,
|
||||||
|
useDeleteQuery,
|
||||||
|
useGetQuery,
|
||||||
|
usePaginatedInfiniteQuery,
|
||||||
|
usePostQuery,
|
||||||
|
} from "@/api/shared";
|
||||||
|
|
||||||
export const useSourceDetails = (sourceId: string) => {
|
export const useSourceDetails = (sourceId: string) => {
|
||||||
return useGetQuery<SourceDetails>(endpoint.feedManagement.getSourceDetails(sourceId));
|
return useGetQuery<SourceDetails>(endpoint.feedManagement.getSourceDetails(sourceId));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSourceOverviewList = (filters: ArticleFilters = {}) => {
|
export const useSourceOverviewList = (filters: ArticleFilters = {}) => {
|
||||||
return usePaginatedInfiniteQuery<SourceOverview>(endpoint.feedManagement.getSourceOverviewList, filters);
|
return usePaginatedInfiniteQuery<SourceOverview>(
|
||||||
|
endpoint.feedManagement.getSourceOverviewList,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSourceArticleOverviewList = (sourceId: string, filters: ArticleFilters = {}) => {
|
export const useSourceArticleOverviewList = (sourceId: string, filters: ArticleFilters = {}) => {
|
||||||
return usePaginatedInfiniteQuery<ArticleOverview>(
|
return usePaginatedInfiniteQuery<ArticleOverview>(
|
||||||
endpoint.feedManagement.getSourceArticleOverviewList(sourceId),
|
endpoint.feedManagement.getSourceArticleOverviewList(sourceId),
|
||||||
filters
|
filters,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export type Bookmark = {
|
|||||||
export type BookmarkedArticle = ArticleOverview;
|
export type BookmarkedArticle = ArticleOverview;
|
||||||
|
|
||||||
export const BookmarkPayloadSchema = Joi.object({
|
export const BookmarkPayloadSchema = Joi.object({
|
||||||
name: Joi.string().required().messages({
|
|
||||||
"string.empty": "Le nom est requis",
|
|
||||||
"any.required": "Le nom est requis",
|
|
||||||
}),
|
|
||||||
description: Joi.string().optional(),
|
description: Joi.string().optional(),
|
||||||
isPublic: Joi.boolean().optional(),
|
isPublic: Joi.boolean().optional(),
|
||||||
|
name: Joi.string().required().messages({
|
||||||
|
"any.required": "Le nom est requis",
|
||||||
|
"string.empty": "Le nom est requis",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ export type RefreshTokenResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LoginPayloadSchema = Joi.object<LoginPayload>({
|
export const LoginPayloadSchema = Joi.object<LoginPayload>({
|
||||||
username: Joi.string().required().messages({
|
|
||||||
"string.empty": "L'email est requis",
|
|
||||||
"any.required": "L'email est requis",
|
|
||||||
}),
|
|
||||||
password: Joi.string().min(4).required().messages({
|
password: Joi.string().min(4).required().messages({
|
||||||
|
"any.required": "Le mot de passe est requis",
|
||||||
"string.empty": "Le mot de passe est requis",
|
"string.empty": "Le mot de passe est requis",
|
||||||
"string.min": "Le mot de passe doit comporter au moins 4 caractères",
|
"string.min": "Le mot de passe doit comporter au moins 4 caractères",
|
||||||
"any.required": "Le mot de passe est requis",
|
}),
|
||||||
|
username: Joi.string().required().messages({
|
||||||
|
"any.required": "L'email est requis",
|
||||||
|
"string.empty": "L'email est requis",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,37 +17,37 @@ export type UpdatePasswordPayload = {
|
|||||||
|
|
||||||
export const RequestPasswordPayloadSchema = Joi.object<RequestPasswordPayload>({
|
export const RequestPasswordPayloadSchema = Joi.object<RequestPasswordPayload>({
|
||||||
email: Joi.string().required().messages({
|
email: Joi.string().required().messages({
|
||||||
"string.empty": "L'email est requis",
|
|
||||||
"any.required": "L'email est requis",
|
"any.required": "L'email est requis",
|
||||||
|
"string.empty": "L'email est requis",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ResetPasswordPayloadSchema = Joi.object<ResetPasswordPayload>({
|
export const ResetPasswordPayloadSchema = Joi.object<ResetPasswordPayload>({
|
||||||
password: Joi.string().min(6).required().messages({
|
|
||||||
"string.empty": "Le mot de passe est requis",
|
|
||||||
"string.min": "Le mot de passe doit comporter au moins 6 caractères",
|
|
||||||
"any.required": "Le mot de passe est requis",
|
|
||||||
}),
|
|
||||||
confirm: Joi.string().valid(Joi.ref("password")).required().messages({
|
confirm: Joi.string().valid(Joi.ref("password")).required().messages({
|
||||||
"any.only": "Les mots de passe ne correspondent pas",
|
"any.only": "Les mots de passe ne correspondent pas",
|
||||||
"string.empty": "La confirmation du mot de passe est requise",
|
|
||||||
"any.required": "La confirmation du mot de passe est requise",
|
"any.required": "La confirmation du mot de passe est requise",
|
||||||
|
"string.empty": "La confirmation du mot de passe est requise",
|
||||||
|
}),
|
||||||
|
password: Joi.string().min(6).required().messages({
|
||||||
|
"any.required": "Le mot de passe est requis",
|
||||||
|
"string.empty": "Le mot de passe est requis",
|
||||||
|
"string.min": "Le mot de passe doit comporter au moins 6 caractères",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdatePasswordPayloadSchema = Joi.object<UpdatePasswordPayload>({
|
export const UpdatePasswordPayloadSchema = Joi.object<UpdatePasswordPayload>({
|
||||||
current: Joi.string().required().messages({
|
|
||||||
"string.empty": "Le mot de passe actuel est requis",
|
|
||||||
"any.required": "Le mot de passe actuel est requis",
|
|
||||||
}),
|
|
||||||
password: Joi.string().min(6).required().messages({
|
|
||||||
"string.empty": "Le nouveau mot de passe est requis",
|
|
||||||
"string.min": "Le nouveau mot de passe doit comporter au moins 6 caractères",
|
|
||||||
"any.required": "Le nouveau mot de passe est requis",
|
|
||||||
}),
|
|
||||||
confirm: Joi.string().valid(Joi.ref("password")).required().messages({
|
confirm: Joi.string().valid(Joi.ref("password")).required().messages({
|
||||||
"any.only": "Les mots de passe ne correspondent pas",
|
"any.only": "Les mots de passe ne correspondent pas",
|
||||||
"string.empty": "La confirmation du nouveau mot de passe est requise",
|
|
||||||
"any.required": "La confirmation du nouveau mot de passe est requise",
|
"any.required": "La confirmation du nouveau mot de passe est requise",
|
||||||
|
"string.empty": "La confirmation du nouveau mot de passe est requise",
|
||||||
|
}),
|
||||||
|
current: Joi.string().required().messages({
|
||||||
|
"any.required": "Le mot de passe actuel est requis",
|
||||||
|
"string.empty": "Le mot de passe actuel est requis",
|
||||||
|
}),
|
||||||
|
password: Joi.string().min(6).required().messages({
|
||||||
|
"any.required": "Le nouveau mot de passe est requis",
|
||||||
|
"string.empty": "Le nouveau mot de passe est requis",
|
||||||
|
"string.min": "Le nouveau mot de passe doit comporter au moins 6 caractères",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ export type RegisterPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RegisterPayloadSchema = Joi.object<RegisterPayload>({
|
export const RegisterPayloadSchema = Joi.object<RegisterPayload>({
|
||||||
name: Joi.string().required().messages({
|
|
||||||
"string.empty": "Le nom est requis",
|
|
||||||
"any.required": "Le nom est requis",
|
|
||||||
}),
|
|
||||||
email: Joi.string().required().messages({
|
email: Joi.string().required().messages({
|
||||||
"string.empty": "L'email est requis",
|
|
||||||
"any.required": "L'email est requis",
|
"any.required": "L'email est requis",
|
||||||
|
"string.empty": "L'email est requis",
|
||||||
|
}),
|
||||||
|
name: Joi.string().required().messages({
|
||||||
|
"any.required": "Le nom est requis",
|
||||||
|
"string.empty": "Le nom est requis",
|
||||||
}),
|
}),
|
||||||
password: Joi.string().min(6).required().messages({
|
password: Joi.string().min(6).required().messages({
|
||||||
|
"any.required": "Le mot de passe est requis",
|
||||||
"string.empty": "Le mot de passe est requis",
|
"string.empty": "Le mot de passe est requis",
|
||||||
"string.min": "Le mot de passe doit comporter au moins 4 caractères",
|
"string.min": "Le mot de passe doit comporter au moins 4 caractères",
|
||||||
"any.required": "Le mot de passe est requis",
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { skipToken, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import {
|
||||||
|
skipToken,
|
||||||
|
useInfiniteQuery,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
|
|
||||||
@@ -45,7 +51,9 @@ export type PaginatedResponse<TItem> = {
|
|||||||
pagination: PaginationInfo;
|
pagination: PaginationInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const safeMessage = (error: AxiosError<ClientErrorResponse | ClientDetailErrorResponse> | Error): string => {
|
export const safeMessage = (
|
||||||
|
error: AxiosError<ClientErrorResponse | ClientDetailErrorResponse> | Error,
|
||||||
|
): string => {
|
||||||
if (error instanceof AxiosError && error.response) {
|
if (error instanceof AxiosError && error.response) {
|
||||||
const response = error.response.data;
|
const response = error.response.data;
|
||||||
|
|
||||||
@@ -59,52 +67,58 @@ export const safeMessage = (error: AxiosError<ClientErrorResponse | ClientDetail
|
|||||||
return "Une erreur est survenue";
|
return "Une erreur est survenue";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePaginatedInfiniteQuery = <TItem>(endpoint: string, filters: PaginationFilters = {}) => {
|
export const usePaginatedInfiniteQuery = <TItem>(
|
||||||
|
endpoint: string,
|
||||||
|
filters: PaginationFilters = {},
|
||||||
|
) => {
|
||||||
return useInfiniteQuery<PaginatedResponse<TItem>, ErrorResponse>({
|
return useInfiniteQuery<PaginatedResponse<TItem>, ErrorResponse>({
|
||||||
|
getNextPageParam: (lastPage: PaginatedResponse<TItem>) => {
|
||||||
|
const { lastId } = lastPage.pagination;
|
||||||
|
return lastId ? lastId : null;
|
||||||
|
},
|
||||||
initialData: undefined,
|
initialData: undefined,
|
||||||
initialPageParam: null,
|
initialPageParam: null,
|
||||||
queryKey: [endpoint, filters],
|
|
||||||
queryFn: async ({ pageParam = null }) => {
|
queryFn: async ({ pageParam = null }) => {
|
||||||
const query = qs.stringify({ ...filters, lastId: pageParam }, { skipNulls: true });
|
const query = qs.stringify({ ...filters, lastId: pageParam }, { skipNulls: true });
|
||||||
const url = `${endpoint}?${query}`;
|
const url = `${endpoint}?${query}`;
|
||||||
const response = await client.get<PaginatedResponse<TItem>>(url);
|
const response = await client.get<PaginatedResponse<TItem>>(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage: PaginatedResponse<TItem>) => {
|
queryKey: [endpoint, filters],
|
||||||
const { lastId } = lastPage.pagination;
|
|
||||||
return lastId ? lastId : null;
|
|
||||||
},
|
|
||||||
staleTime: 1_000 * 60 * 10,
|
staleTime: 1_000 * 60 * 10,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePaginatedQuery = <TItem>(endpoint: string, filters: PaginationFilters = {}) => {
|
export const usePaginatedQuery = <TItem>(endpoint: string, filters: PaginationFilters = {}) => {
|
||||||
return useQuery<PaginatedResponse<TItem>, ErrorResponse>({
|
return useQuery<PaginatedResponse<TItem>, ErrorResponse>({
|
||||||
queryKey: [endpoint, filters],
|
|
||||||
queryFn: async (): Promise<PaginatedResponse<TItem>> => {
|
queryFn: async (): Promise<PaginatedResponse<TItem>> => {
|
||||||
const query = qs.stringify({ ...filters, lastId: null }, { skipNulls: true });
|
const query = qs.stringify({ ...filters, lastId: null }, { skipNulls: true });
|
||||||
const url = `${endpoint}?${query}`;
|
const url = `${endpoint}?${query}`;
|
||||||
const response = await client.get<PaginatedResponse<TItem>>(url);
|
const response = await client.get<PaginatedResponse<TItem>>(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
queryKey: [endpoint, filters],
|
||||||
staleTime: 1_000 * 60 * 10,
|
staleTime: 1_000 * 60 * 10,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetQuery = <TItem>(endpoint: string, enabled: boolean = true) => {
|
export const useGetQuery = <TItem>(endpoint: string, enabled: boolean = true) => {
|
||||||
return useQuery<TItem, ErrorResponse>({
|
return useQuery<TItem, ErrorResponse>({
|
||||||
queryKey: [endpoint],
|
|
||||||
queryFn: enabled
|
queryFn: enabled
|
||||||
? async (): Promise<TItem> => {
|
? async (): Promise<TItem> => {
|
||||||
const response = await client.get<TItem>(endpoint);
|
const response = await client.get<TItem>(endpoint);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
: skipToken,
|
: skipToken,
|
||||||
|
queryKey: [endpoint],
|
||||||
staleTime: 1_000 * 60 * 10,
|
staleTime: 1_000 * 60 * 10,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePostQuery = <TPayload = void, TResponse = void>(endpoint: string, keys: string[] = []) => {
|
export const usePostQuery = <TPayload = void, TResponse = void>(
|
||||||
|
endpoint: string,
|
||||||
|
keys: string[] = [],
|
||||||
|
) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TResponse, ErrorResponse, TPayload>({
|
return useMutation<TResponse, ErrorResponse, TPayload>({
|
||||||
mutationFn: async (data: TPayload): Promise<TResponse> => {
|
mutationFn: async (data: TPayload): Promise<TResponse> => {
|
||||||
@@ -119,7 +133,10 @@ export const usePostQuery = <TPayload = void, TResponse = void>(endpoint: string
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePutQuery = <TPayload = void, TResponse = void>(endpoint: string, keys: string[] = []) => {
|
export const usePutQuery = <TPayload = void, TResponse = void>(
|
||||||
|
endpoint: string,
|
||||||
|
keys: string[] = [],
|
||||||
|
) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TResponse, ErrorResponse, TPayload>({
|
return useMutation<TResponse, ErrorResponse, TPayload>({
|
||||||
mutationFn: async (data: TPayload): Promise<TResponse> => {
|
mutationFn: async (data: TPayload): Promise<TResponse> => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { BookMarked, Globe, Home, User } from "@tamagui/lucide-icons";
|
import { BookMarked, Globe, Home, User } from "@tamagui/lucide-icons";
|
||||||
import { Tabs } from "expo-router";
|
import { Tabs } from "expo-router";
|
||||||
import { useColorScheme } from "react-native";
|
import { useColorScheme } from "react-native";
|
||||||
@@ -13,68 +11,68 @@ export default function TabLayout() {
|
|||||||
initialRouteName="articles"
|
initialRouteName="articles"
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarShowLabel: true,
|
|
||||||
tabBarActiveTintColor: "$accent5",
|
tabBarActiveTintColor: "$accent5",
|
||||||
tabBarHideOnKeyboard: true,
|
tabBarHideOnKeyboard: true,
|
||||||
|
tabBarLabelStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
textTransform: "none",
|
||||||
|
},
|
||||||
|
tabBarShowLabel: true,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: colorScheme === "dark" ? "black" : "white",
|
backgroundColor: colorScheme === "dark" ? "black" : "white",
|
||||||
borderTopWidth: 0,
|
borderTopWidth: 0,
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
paddingTop: 5,
|
paddingTop: 5,
|
||||||
},
|
},
|
||||||
tabBarLabelStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: "600",
|
|
||||||
textTransform: "none",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="articles"
|
name="articles"
|
||||||
options={{
|
options={{
|
||||||
href: "/(authed)/(tabs)/articles",
|
href: "/(authed)/(tabs)/articles",
|
||||||
|
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
|
||||||
tabBarLabel: ({ color }) => (
|
tabBarLabel: ({ color }) => (
|
||||||
<Paragraph size="$2" color={color}>
|
<Paragraph color={color} size="$2">
|
||||||
Actualités
|
Actualités
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="sources"
|
name="sources"
|
||||||
options={{
|
options={{
|
||||||
href: "/(authed)/(tabs)/sources",
|
href: "/(authed)/(tabs)/sources",
|
||||||
|
tabBarIcon: ({ color, size }) => <Globe color={color} size={size} />,
|
||||||
tabBarLabel: ({ color }) => (
|
tabBarLabel: ({ color }) => (
|
||||||
<Paragraph size="$2" color={color}>
|
<Paragraph color={color} size="$2">
|
||||||
Sources
|
Sources
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <Globe size={size} color={color} />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="bookmarks"
|
name="bookmarks"
|
||||||
options={{
|
options={{
|
||||||
href: "/(authed)/(tabs)/bookmarks",
|
href: "/(authed)/(tabs)/bookmarks",
|
||||||
|
tabBarIcon: ({ color, size }) => <BookMarked color={color} size={size} />,
|
||||||
tabBarLabel: ({ color }) => (
|
tabBarLabel: ({ color }) => (
|
||||||
<Paragraph size="$2" color={color}>
|
<Paragraph color={color} size="$2">
|
||||||
Signets
|
Signets
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <BookMarked size={size} color={color} />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="account"
|
name="account"
|
||||||
options={{
|
options={{
|
||||||
href: "/(authed)/(tabs)/account",
|
href: "/(authed)/(tabs)/account",
|
||||||
|
tabBarIcon: ({ color, size }) => <User color={color} size={size} />,
|
||||||
tabBarLabel: ({ color }) => (
|
tabBarLabel: ({ color }) => (
|
||||||
<Paragraph size="$2" color={color}>
|
<Paragraph color={color} size="$2">
|
||||||
Profil
|
Profil
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export default function Index() {
|
|||||||
<YGroup alignSelf="center" bordered size="$4">
|
<YGroup alignSelf="center" bordered size="$4">
|
||||||
<YGroup.Item>
|
<YGroup.Item>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/account/settings")}
|
|
||||||
icon={Settings}
|
icon={Settings}
|
||||||
iconAfter={ChevronRight}
|
iconAfter={ChevronRight}
|
||||||
|
onPress={() => router.push("/account/settings")}
|
||||||
title="Settings"
|
title="Settings"
|
||||||
/>
|
/>
|
||||||
</YGroup.Item>
|
</YGroup.Item>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export default function Index() {
|
|||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
mutate(undefined, {
|
mutate(undefined, {
|
||||||
onSuccess: () => authState.logout(),
|
|
||||||
onError: () => authState.logout(),
|
onError: () => authState.logout(),
|
||||||
|
onSuccess: () => authState.logout(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,9 +23,9 @@ export default function Index() {
|
|||||||
<YStack width="100%">
|
<YStack width="100%">
|
||||||
<Button
|
<Button
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
|
fontWeight="bold"
|
||||||
onPress={handleLogout}
|
onPress={handleLogout}
|
||||||
theme={isPending ? "disabled" : "accent"}
|
theme={isPending ? "disabled" : "accent"}
|
||||||
fontWeight="bold"
|
|
||||||
>
|
>
|
||||||
{isPending ? <ActivityIndicator /> : "Déconnexion"}
|
{isPending ? <ActivityIndicator /> : "Déconnexion"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { ArticleCategoryPill, ArticleCoverImage } from "@/ui/components/content/
|
|||||||
import { SourceReferencePill } from "@/ui/components/content/source";
|
import { SourceReferencePill } from "@/ui/components/content/source";
|
||||||
import { BackButton } from "@/ui/components/controls/BackButton";
|
import { BackButton } from "@/ui/components/controls/BackButton";
|
||||||
import { IconButton } from "@/ui/components/controls/IconButton";
|
import { IconButton } from "@/ui/components/controls/IconButton";
|
||||||
import { ScreenView } from "@/ui/components/layout";
|
|
||||||
import { LoadingView } from "@/ui/components/LoadingView";
|
import { LoadingView } from "@/ui/components/LoadingView";
|
||||||
|
import { ScreenView } from "@/ui/components/layout";
|
||||||
import { Caption, Text } from "@/ui/components/typography";
|
import { Caption, Text } from "@/ui/components/typography";
|
||||||
|
|
||||||
export default function ArticleDetails() {
|
export default function ArticleDetails() {
|
||||||
@@ -29,9 +29,9 @@ export default function ArticleDetails() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "error",
|
|
||||||
text1: "Erreur",
|
text1: "Erreur",
|
||||||
text2: safeMessage(error),
|
text2: safeMessage(error),
|
||||||
|
type: "error",
|
||||||
});
|
});
|
||||||
router.replace("/(authed)/(tabs)/articles");
|
router.replace("/(authed)/(tabs)/articles");
|
||||||
}
|
}
|
||||||
@@ -46,22 +46,27 @@ export default function ArticleDetails() {
|
|||||||
leadingAction={<BackButton onPress={() => router.dismissTo("/(authed)/(tabs)/articles")} />}
|
leadingAction={<BackButton onPress={() => router.dismissTo("/(authed)/(tabs)/articles")} />}
|
||||||
trailingActions={
|
trailingActions={
|
||||||
<>
|
<>
|
||||||
<IconButton onPress={() => {}} icon={<Bookmark size="$1" />} />
|
<IconButton icon={<Bookmark size="$1" />} onPress={() => {}} />
|
||||||
<IconButton onPress={() => {}} icon={<Share size="$1" />} />
|
<IconButton icon={<Share size="$1" />} onPress={() => {}} />
|
||||||
<IconButton onPress={() => {}} icon={<MoreVertical size="$1" />} />
|
<IconButton icon={<MoreVertical size="$1" />} onPress={() => {}} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<YStack>
|
<YStack>
|
||||||
{article.metadata?.image && (
|
{article.metadata?.image && (
|
||||||
<ArticleCoverImage uri={article.metadata.image} width="100%" height={225} marginBottom="$4" />
|
<ArticleCoverImage
|
||||||
|
height={225}
|
||||||
|
marginBottom="$4"
|
||||||
|
uri={article.metadata.image}
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack gap="$4" backgroundColor="$background">
|
<YStack backgroundColor="$background" gap="$4">
|
||||||
<XStack gap="$2" flexWrap="wrap">
|
<XStack flexWrap="wrap" gap="$2">
|
||||||
{article.categories.map((category, index) => (
|
{article.categories.map((category, index) => (
|
||||||
<ArticleCategoryPill key={index} category={category.toLowerCase()} />
|
<ArticleCategoryPill category={category.toLowerCase()} key={index} />
|
||||||
))}
|
))}
|
||||||
</XStack>
|
</XStack>
|
||||||
<H5 fontWeight="bold" marginBottom="$1">
|
<H5 fontWeight="bold" marginBottom="$1">
|
||||||
@@ -70,18 +75,18 @@ export default function ArticleDetails() {
|
|||||||
|
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<SourceReferencePill data={article.source} />
|
<SourceReferencePill data={article.source} />
|
||||||
<XStack height={20} alignItems="center">
|
<XStack alignItems="center" height={20}>
|
||||||
<Caption>{relativeTime}</Caption>
|
<Caption>{relativeTime}</Caption>
|
||||||
<Separator alignSelf="stretch" vertical marginHorizontal={16} />
|
<Separator alignSelf="stretch" marginHorizontal={16} vertical />
|
||||||
<Caption>{article.readingTime} minutes de lecture</Caption>
|
<Caption>{article.readingTime} minutes de lecture</Caption>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<Text size="$3" marginTop="$2">
|
<Text marginTop="$2" size="$3">
|
||||||
{article.body.trim()}
|
{article.body.trim()}
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
<Button width="100%" onPress={handleReadIntegrality} theme="accent" fontWeight="bold">
|
<Button fontWeight="bold" onPress={handleReadIntegrality} theme="accent" width="100%">
|
||||||
Consulter l'article
|
Consulter l'article
|
||||||
</Button>
|
</Button>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ScrollView, YStack } from "tamagui";
|
import { ScrollView, YStack } from "tamagui";
|
||||||
|
|
||||||
import { useArticleOverviewList } from "@/api/request/feed-management/article";
|
import { useArticleOverviewList } from "@/api/request/feed-management/article";
|
||||||
@@ -24,24 +22,27 @@ export default function Index() {
|
|||||||
<ScrollView contentContainerStyle={{ paddingBottom: 0 }}>
|
<ScrollView contentContainerStyle={{ paddingBottom: 0 }}>
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<ScreenView.Section title="Tendances" forwardLink="/(authed)/(tabs)/articles/trending" />
|
<ScreenView.Section
|
||||||
|
forwardLink="/(authed)/(tabs)/articles/trending"
|
||||||
|
title="Tendances"
|
||||||
|
/>
|
||||||
|
|
||||||
{articlesLoading && <ArticleSkeletonList displayMode="card" horizontal={true} />}
|
{articlesLoading && <ArticleSkeletonList displayMode="card" horizontal={true} />}
|
||||||
{!articlesLoading && (
|
{!articlesLoading && (
|
||||||
<ArticleList
|
<ArticleList
|
||||||
data={articleOverviews}
|
data={articleOverviews}
|
||||||
refreshing={articlesLoading}
|
|
||||||
displayMode="card"
|
displayMode="card"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
refreshing={articlesLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<ScreenView.Section title="Nos sources" forwardLink="/(authed)/(tabs)/sources" />
|
<ScreenView.Section forwardLink="/(authed)/(tabs)/sources" title="Nos sources" />
|
||||||
|
|
||||||
{sourcesLoading && <SourceSkeletonList horizontal={true} />}
|
{sourcesLoading && <SourceSkeletonList horizontal={true} />}
|
||||||
{!sourcesLoading && (
|
{!sourcesLoading && (
|
||||||
<SourceList data={sourcesOverviews} refreshing={sourcesLoading} horizontal={true} />
|
<SourceList data={sourcesOverviews} horizontal={true} refreshing={sourcesLoading} />
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
import { useInfiniteArticleOverviewList } from "@/api/request/feed-management/article";
|
import { useInfiniteArticleOverviewList } from "@/api/request/feed-management/article";
|
||||||
@@ -11,9 +9,8 @@ import { ScreenView } from "@/ui/components/layout";
|
|||||||
|
|
||||||
export default function Trending() {
|
export default function Trending() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteArticleOverviewList(
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } =
|
||||||
{ limit: 20 }
|
useInfiniteArticleOverviewList({ limit: 20 });
|
||||||
);
|
|
||||||
const articles: TrendingArticle[] = useFlattenedItems(data);
|
const articles: TrendingArticle[] = useFlattenedItems(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,10 +26,10 @@ export default function Trending() {
|
|||||||
data={articles}
|
data={articles}
|
||||||
fetchNextPage={fetchNextPage}
|
fetchNextPage={fetchNextPage}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
|
||||||
refreshing={isLoading}
|
|
||||||
onRefresh={refetch}
|
|
||||||
infiniteScroll={true}
|
infiniteScroll={true}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
onRefresh={refetch}
|
||||||
|
refreshing={isLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScreenView>
|
</ScreenView>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Plus, Search } from "@tamagui/lucide-icons";
|
import { Plus, Search } from "@tamagui/lucide-icons";
|
||||||
import { YStack } from "tamagui";
|
import { YStack } from "tamagui";
|
||||||
|
|
||||||
@@ -8,19 +6,20 @@ import { Bookmark } from "@/api/schema/feed-management/bookmark";
|
|||||||
import { useFlattenedItems } from "@/hooks/use-flattened-items";
|
import { useFlattenedItems } from "@/hooks/use-flattened-items";
|
||||||
import { BookmarkList } from "@/ui/components/content/bookmark";
|
import { BookmarkList } from "@/ui/components/content/bookmark";
|
||||||
import { IconButton } from "@/ui/components/controls/IconButton";
|
import { IconButton } from "@/ui/components/controls/IconButton";
|
||||||
import { ScreenView } from "@/ui/components/layout";
|
|
||||||
import { LoadingView } from "@/ui/components/LoadingView";
|
import { LoadingView } from "@/ui/components/LoadingView";
|
||||||
|
import { ScreenView } from "@/ui/components/layout";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useBookmarkList();
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } =
|
||||||
|
useBookmarkList();
|
||||||
const bookmarks: Bookmark[] = useFlattenedItems(data);
|
const bookmarks: Bookmark[] = useFlattenedItems(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenView>
|
<ScreenView>
|
||||||
<ScreenView.Heading
|
<ScreenView.Heading
|
||||||
|
leadingAction={<IconButton icon={<Plus size="$1" />} onPress={() => {}} />}
|
||||||
title="Bookmarks"
|
title="Bookmarks"
|
||||||
leadingAction={<IconButton onPress={() => {}} icon={<Plus size="$1" />} />}
|
trailingActions={<IconButton icon={<Search size="$1" />} onPress={() => {}} />}
|
||||||
trailingActions={<IconButton onPress={() => {}} icon={<Search size="$1" />} />}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YStack width="100%">
|
<YStack width="100%">
|
||||||
@@ -28,12 +27,12 @@ export default function Index() {
|
|||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<BookmarkList
|
<BookmarkList
|
||||||
data={bookmarks}
|
data={bookmarks}
|
||||||
refreshing={isLoading}
|
|
||||||
onRefresh={refetch}
|
|
||||||
infiniteScroll={true}
|
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
|
||||||
fetchNextPage={fetchNextPage}
|
fetchNextPage={fetchNextPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
infiniteScroll={true}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
onRefresh={refetch}
|
||||||
|
refreshing={isLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { joiResolver } from "@hookform/resolvers/joi";
|
import { joiResolver } from "@hookform/resolvers/joi";
|
||||||
import { Link, useRouter } from "expo-router";
|
import { Link, useRouter } from "expo-router";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -7,7 +5,10 @@ import Toast from "react-native-toast-message";
|
|||||||
import { YStack } from "tamagui";
|
import { YStack } from "tamagui";
|
||||||
|
|
||||||
import { usePasswordForgotten } from "@/api/request/identity-and-access/password";
|
import { usePasswordForgotten } from "@/api/request/identity-and-access/password";
|
||||||
import { RequestPasswordPayload, RequestPasswordPayloadSchema } from "@/api/schema/identity-and-access/password";
|
import {
|
||||||
|
RequestPasswordPayload,
|
||||||
|
RequestPasswordPayloadSchema,
|
||||||
|
} from "@/api/schema/identity-and-access/password";
|
||||||
import { ErrorResponse, safeMessage } from "@/api/shared";
|
import { ErrorResponse, safeMessage } from "@/api/shared";
|
||||||
import { FormEmailInput } from "@/ui/components/controls/forms";
|
import { FormEmailInput } from "@/ui/components/controls/forms";
|
||||||
import { SubmitButton } from "@/ui/components/controls/SubmitButton";
|
import { SubmitButton } from "@/ui/components/controls/SubmitButton";
|
||||||
@@ -24,6 +25,13 @@ export default function PasswordRequest() {
|
|||||||
|
|
||||||
const onSubmit = (data: RequestPasswordPayload) => {
|
const onSubmit = (data: RequestPasswordPayload) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
|
onError: (error: ErrorResponse) => {
|
||||||
|
Toast.show({
|
||||||
|
text1: "Erreur de connexion",
|
||||||
|
text2: safeMessage(error),
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
text1: "Succès",
|
text1: "Succès",
|
||||||
@@ -32,37 +40,31 @@ export default function PasswordRequest() {
|
|||||||
});
|
});
|
||||||
router.push("/(unauthed)/signin");
|
router.push("/(unauthed)/signin");
|
||||||
},
|
},
|
||||||
onError: (error: ErrorResponse) => {
|
|
||||||
Toast.show({
|
|
||||||
text1: "Erreur de connexion",
|
|
||||||
text2: safeMessage(error),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenView>
|
<ScreenView>
|
||||||
<YStack flex={1} gap="$4" width="100%" justifyContent="flex-start">
|
<YStack flex={1} gap="$4" justifyContent="flex-start" width="100%">
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<Heading>Mot de passe oublié ?</Heading>
|
<Heading>Mot de passe oublié ?</Heading>
|
||||||
<Text>
|
<Text>
|
||||||
Veuillez entrer votre adresse e-mail pour recevoir un lien de réinitialisation de mot de passe.
|
Veuillez entrer votre adresse e-mail pour recevoir un lien de réinitialisation de mot de
|
||||||
|
passe.
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<FormEmailInput control={control} name="email" />
|
<FormEmailInput control={control} name="email" />
|
||||||
|
|
||||||
<Link href="/signin" asChild>
|
<Link asChild href="/signin">
|
||||||
<Text>Vous avez pas de compte ? Se connecter</Text>
|
<Text>Vous avez pas de compte ? Se connecter</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</YStack>
|
</YStack>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
label="Réinitialiser le mot de passe"
|
|
||||||
onPress={handleSubmit(onSubmit)}
|
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
isValid={formState.isValid}
|
isValid={formState.isValid}
|
||||||
|
label="Réinitialiser le mot de passe"
|
||||||
|
onPress={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
</ScreenView>
|
</ScreenView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { joiResolver } from "@hookform/resolvers/joi";
|
import { joiResolver } from "@hookform/resolvers/joi";
|
||||||
import { Link, useRouter } from "expo-router";
|
import { Link, useRouter } from "expo-router";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -7,7 +5,11 @@ import Toast from "react-native-toast-message";
|
|||||||
import { YStack } from "tamagui";
|
import { YStack } from "tamagui";
|
||||||
|
|
||||||
import { useLogin } from "@/api/request/identity-and-access/login";
|
import { useLogin } from "@/api/request/identity-and-access/login";
|
||||||
import { LoginPayload, LoginPayloadSchema, LoginResponse } from "@/api/schema/identity-and-access/login";
|
import {
|
||||||
|
LoginPayload,
|
||||||
|
LoginPayloadSchema,
|
||||||
|
LoginResponse,
|
||||||
|
} from "@/api/schema/identity-and-access/login";
|
||||||
import { ErrorResponse, safeMessage } from "@/api/shared";
|
import { ErrorResponse, safeMessage } from "@/api/shared";
|
||||||
import { useAuth } from "@/providers/auth-provider";
|
import { useAuth } from "@/providers/auth-provider";
|
||||||
import { FormEmailInput, FormPasswordInput } from "@/ui/components/controls/forms";
|
import { FormEmailInput, FormPasswordInput } from "@/ui/components/controls/forms";
|
||||||
@@ -30,10 +32,6 @@ export default function SignIn() {
|
|||||||
|
|
||||||
const onSubmit = (data: LoginPayload) => {
|
const onSubmit = (data: LoginPayload) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async (data: LoginResponse) => {
|
|
||||||
auth.login(data.token, data.refresh_token);
|
|
||||||
Toast.show({ text1: "Connexion réussie", type: "success" });
|
|
||||||
},
|
|
||||||
onError: (error: ErrorResponse) => {
|
onError: (error: ErrorResponse) => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
text1: "Erreur de connexion",
|
text1: "Erreur de connexion",
|
||||||
@@ -41,12 +39,16 @@ export default function SignIn() {
|
|||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onSuccess: async (data: LoginResponse) => {
|
||||||
|
auth.login(data.token, data.refresh_token);
|
||||||
|
Toast.show({ text1: "Connexion réussie", type: "success" });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenView>
|
<ScreenView>
|
||||||
<YStack flex={1} gap="$4" width="100%" justifyContent="flex-start">
|
<YStack flex={1} gap="$4" justifyContent="flex-start" width="100%">
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<Heading>Connexion</Heading>
|
<Heading>Connexion</Heading>
|
||||||
<Text>Bienvenue sur Basango, la plateforme d'actualités intelligente</Text>
|
<Text>Bienvenue sur Basango, la plateforme d'actualités intelligente</Text>
|
||||||
@@ -56,24 +58,24 @@ export default function SignIn() {
|
|||||||
<FormEmailInput control={control} name="username" />
|
<FormEmailInput control={control} name="username" />
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<FormPasswordInput control={control} name="password" />
|
<FormPasswordInput control={control} name="password" />
|
||||||
<Link href="/password-request" asChild>
|
<Link asChild href="/password-request">
|
||||||
<Text color="$accent6"> Mot de passe oublié ?</Text>
|
<Text color="$accent6"> Mot de passe oublié ?</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</YStack>
|
</YStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<Caption>
|
<Caption>
|
||||||
En continuant, vous acceptez les conditions d'utilisation de Basango et reconnaissez avoir lu
|
En continuant, vous acceptez les conditions d'utilisation de Basango et reconnaissez
|
||||||
notre politique de confidentialité.
|
avoir lu notre politique de confidentialité.
|
||||||
</Caption>
|
</Caption>
|
||||||
<Link href="/signup" asChild>
|
<Link asChild href="/signup">
|
||||||
<Text>Vous n'avez pas de compte ? Créer un compte</Text>
|
<Text>Vous n'avez pas de compte ? Créer un compte</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</YStack>
|
</YStack>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
label="Se connecter"
|
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
isValid={formState.isValid}
|
isValid={formState.isValid}
|
||||||
|
label="Se connecter"
|
||||||
onPress={handleSubmit(onSubmit)}
|
onPress={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
</ScreenView>
|
</ScreenView>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { joiResolver } from "@hookform/resolvers/joi";
|
import { joiResolver } from "@hookform/resolvers/joi";
|
||||||
import { User } from "@tamagui/lucide-icons";
|
import { User } from "@tamagui/lucide-icons";
|
||||||
import { Link, useRouter } from "expo-router";
|
import { Link, useRouter } from "expo-router";
|
||||||
@@ -25,6 +23,13 @@ export default function SingUp() {
|
|||||||
|
|
||||||
const onSubmit = (data: RegisterPayload) => {
|
const onSubmit = (data: RegisterPayload) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
|
onError: (error: ErrorResponse) => {
|
||||||
|
Toast.show({
|
||||||
|
text1: "Erreur",
|
||||||
|
text2: safeMessage(error),
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
text1: "Félicitations !",
|
text1: "Félicitations !",
|
||||||
@@ -33,19 +38,12 @@ export default function SingUp() {
|
|||||||
});
|
});
|
||||||
router.replace("/(unauthed)/signin");
|
router.replace("/(unauthed)/signin");
|
||||||
},
|
},
|
||||||
onError: (error: ErrorResponse) => {
|
|
||||||
Toast.show({
|
|
||||||
text1: "Erreur",
|
|
||||||
text2: safeMessage(error),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenView>
|
<ScreenView>
|
||||||
<YStack flex={1} gap="$4" width="100%" justifyContent="flex-start">
|
<YStack flex={1} gap="$4" justifyContent="flex-start" width="100%">
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<Heading>Inscription</Heading>
|
<Heading>Inscription</Heading>
|
||||||
<Text>Rejoignez la communauté Basango et restez informé des dernières actualités</Text>
|
<Text>Rejoignez la communauté Basango et restez informé des dernières actualités</Text>
|
||||||
@@ -54,27 +52,27 @@ export default function SingUp() {
|
|||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
control={control}
|
control={control}
|
||||||
name="name"
|
|
||||||
leadingAdornment={User}
|
|
||||||
label="Nom complet"
|
label="Nom complet"
|
||||||
|
leadingAdornment={User}
|
||||||
|
name="name"
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
/>
|
/>
|
||||||
<FormEmailInput control={control} name="email" />
|
<FormEmailInput control={control} name="email" />
|
||||||
<FormPasswordInput control={control} name="password" />
|
<FormPasswordInput control={control} name="password" />
|
||||||
</YStack>
|
</YStack>
|
||||||
<Caption>
|
<Caption>
|
||||||
En continuant, vous acceptez les conditions d'utilisation de Basango et reconnaissez avoir lu
|
En continuant, vous acceptez les conditions d'utilisation de Basango et reconnaissez
|
||||||
notre politique de confidentialité.
|
avoir lu notre politique de confidentialité.
|
||||||
</Caption>
|
</Caption>
|
||||||
<Link href="/signin">
|
<Link href="/signin">
|
||||||
<Text>Vous avez un compte ? Connectez-vous</Text>
|
<Text>Vous avez un compte ? Connectez-vous</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</YStack>
|
</YStack>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
label="Créer un compte"
|
|
||||||
onPress={handleSubmit(onSubmit)}
|
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
isValid={formState.isValid}
|
isValid={formState.isValid}
|
||||||
|
label="Créer un compte"
|
||||||
|
onPress={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
</ScreenView>
|
</ScreenView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,28 +10,28 @@ export default function Welcome() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenView justifyContent="center">
|
<ScreenView justifyContent="center">
|
||||||
<AppIcon width={120} height={120} />
|
<AppIcon height={120} width={120} />
|
||||||
<YStack width="100%" gap="$6">
|
<YStack gap="$6" width="100%">
|
||||||
<YStack gap="$3">
|
<YStack gap="$3">
|
||||||
<Display textAlign="center">Bienvenue sur Basango</Display>
|
<Display textAlign="center">Bienvenue sur Basango</Display>
|
||||||
<Text textAlign="center" lineHeight="$1" marginTop="auto">
|
<Text lineHeight="$1" marginTop="auto" textAlign="center">
|
||||||
La première plateforme d'actualités intelligente qui vous aide à rester informé sur
|
La première plateforme d'actualités intelligente qui vous aide à rester informé sur
|
||||||
congolaise et internationale.
|
congolaise et internationale.
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<Button onPress={() => router.push("/signin")} theme="accent" fontWeight="bold">
|
<Button fontWeight="bold" onPress={() => router.push("/signin")} theme="accent">
|
||||||
Se connecter
|
Se connecter
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/signup" asChild>
|
<Link asChild href="/signup">
|
||||||
<Text textAlign="center">Ouvrir un compte</Text>
|
<Text textAlign="center">Ouvrir un compte</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<Caption textAlign="center">
|
<Caption textAlign="center">
|
||||||
En continuant, vous acceptez les conditions d'utilisation de Basango et reconnaissez avoir lu
|
En continuant, vous acceptez les conditions d'utilisation de Basango et reconnaissez
|
||||||
notre politique de confidentialité.
|
avoir lu notre politique de confidentialité.
|
||||||
</Caption>
|
</Caption>
|
||||||
</YStack>
|
</YStack>
|
||||||
</ScreenView>
|
</ScreenView>
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ export default function NotFoundScreen() {
|
|||||||
return (
|
return (
|
||||||
<ScreenView>
|
<ScreenView>
|
||||||
<Stack.Screen options={{ title: "Oops !" }} />
|
<Stack.Screen options={{ title: "Oops !" }} />
|
||||||
<View flex={1} backgroundColor="$background" padding="$4">
|
<View backgroundColor="$background" flex={1} padding="$4">
|
||||||
<YStack alignItems="center" justifyContent="center" flex={1} gap="$4">
|
<YStack alignItems="center" flex={1} gap="$4" justifyContent="center">
|
||||||
<AppIcon width={100} height={100} />
|
<AppIcon height={100} width={100} />
|
||||||
<YStack width="100%" gap="$6" alignItems="center" paddingHorizontal="$4">
|
<YStack alignItems="center" gap="$6" paddingHorizontal="$4" width="100%">
|
||||||
<YStack>
|
<YStack>
|
||||||
<Heading fontWeight="bold" lineHeight="$8" textAlign="center">
|
<Heading fontWeight="bold" lineHeight="$8" textAlign="center">
|
||||||
Une erreur s'est produite
|
Une erreur s'est produite
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text textAlign="center" lineHeight="$1" marginTop="auto">
|
<Text lineHeight="$1" marginTop="auto" textAlign="center">
|
||||||
Nous avons une difficulté à charger la page que vous recherchez.
|
Nous avons une difficulté à charger la page que vous recherchez.
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import * as Sentry from "@sentry/react-native";
|
import * as Sentry from "@sentry/react-native";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
|
import React from "react";
|
||||||
import { useColorScheme } from "react-native";
|
import { useColorScheme } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
@@ -12,12 +11,12 @@ import { RootProviders } from "@/providers/root-providers";
|
|||||||
export { ErrorBoundary } from "expo-router";
|
export { ErrorBoundary } from "expo-router";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
debug: __DEV__,
|
||||||
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
|
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
|
||||||
sendDefaultPii: true,
|
sendDefaultPii: true,
|
||||||
debug: __DEV__,
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
tracePropagationTargets: [/.*?/],
|
|
||||||
spotlight: __DEV__,
|
spotlight: __DEV__,
|
||||||
|
tracePropagationTargets: [/.*?/],
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RootLayout() {
|
function RootLayout() {
|
||||||
@@ -29,7 +28,7 @@ function RootLayout() {
|
|||||||
<RootProviders>
|
<RootProviders>
|
||||||
<Theme name={colorScheme || "dark"}>
|
<Theme name={colorScheme || "dark"}>
|
||||||
<Stack screenOptions={{ headerShown: false }} />
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
<Toast topOffset={insets.top + 10} position="top" visibilityTime={6_000} />
|
<Toast position="top" topOffset={insets.top + 10} visibilityTime={6_000} />
|
||||||
</Theme>
|
</Theme>
|
||||||
</RootProviders>
|
</RootProviders>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
|||||||
@@ -9,5 +9,9 @@ export default function Index() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth.isLoggedIn ? <Redirect href="/(authed)/(tabs)/articles" /> : <Redirect href="/(unauthed)/welcome" />;
|
return auth.isLoggedIn ? (
|
||||||
|
<Redirect href="/(authed)/(tabs)/articles" />
|
||||||
|
) : (
|
||||||
|
<Redirect href="/(unauthed)/welcome" />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import Svg, { Circle, G, Path, Rect, SvgProps } from "react-native-svg";
|
import Svg, { Circle, G, Path, Rect, SvgProps } from "react-native-svg";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +71,13 @@ export default function BookmarkIllustration(props: SvgProps) {
|
|||||||
d="M249 439a2.51 2.51 0 11-2.52-2.48A2.5 2.5 0 01249 439zM263 438.89a2.5 2.5 0 11-2.52-2.48 2.52 2.52 0 012.52 2.48z"
|
d="M249 439a2.51 2.51 0 11-2.52-2.48A2.5 2.5 0 01249 439zM263 438.89a2.5 2.5 0 11-2.52-2.48 2.52 2.52 0 012.52 2.48z"
|
||||||
fill="#263238"
|
fill="#263238"
|
||||||
/>
|
/>
|
||||||
<Circle cx={274.51} cy={438.8} r={2.5} transform="rotate(-45.69 274.488 438.779)" fill="#263238" />
|
<Circle
|
||||||
|
cx={274.51}
|
||||||
|
cy={438.8}
|
||||||
|
fill="#263238"
|
||||||
|
r={2.5}
|
||||||
|
transform="rotate(-45.69 274.488 438.779)"
|
||||||
|
/>
|
||||||
<Path d="M197.74 318.91H318.79V326.82000000000005H197.74z" fill="#23a99c" />
|
<Path d="M197.74 318.91H318.79V326.82000000000005H197.74z" fill="#23a99c" />
|
||||||
<Path
|
<Path
|
||||||
d="M317.06 344.92c0 .28-27.18.52-60.69.52s-60.7-.24-60.7-.52 27.17-.52 60.7-.52 60.69.23 60.69.52zM317.06 359.32c0 .28-27.18.51-60.69.51s-60.7-.23-60.7-.51 27.17-.52 60.7-.52 60.69.2 60.69.52zM317.06 373.71c0 .29-27.18.52-60.69.52s-60.7-.23-60.7-.52 27.17-.51 60.7-.51 60.69.23 60.69.51zM317.06 388.11c0 .28-27.18.52-60.69.52s-60.7-.24-60.7-.52 27.17-.52 60.7-.52 60.69.23 60.69.52zM317.06 402.5c0 .29-27.18.52-60.69.52s-60.7-.23-60.7-.52 27.17-.51 60.7-.51 60.69.23 60.69.51zM318.79 418.83c0 .28-27.17.51-60.68.51s-60.7-.23-60.7-.51 27.17-.52 60.7-.52 60.68.23 60.68.52z"
|
d="M317.06 344.92c0 .28-27.18.52-60.69.52s-60.7-.24-60.7-.52 27.17-.52 60.7-.52 60.69.23 60.69.52zM317.06 359.32c0 .28-27.18.51-60.69.51s-60.7-.23-60.7-.51 27.17-.52 60.7-.52 60.69.2 60.69.52zM317.06 373.71c0 .29-27.18.52-60.69.52s-60.7-.23-60.7-.52 27.17-.51 60.7-.51 60.69.23 60.69.51zM317.06 388.11c0 .28-27.18.52-60.69.52s-60.7-.24-60.7-.52 27.17-.52 60.7-.52 60.69.23 60.69.52zM317.06 402.5c0 .29-27.18.52-60.69.52s-60.7-.23-60.7-.52 27.17-.51 60.7-.51 60.69.23 60.69.51zM318.79 418.83c0 .28-27.17.51-60.68.51s-60.7-.23-60.7-.51 27.17-.52 60.7-.52 60.68.23 60.68.52z"
|
||||||
@@ -103,7 +107,7 @@ export default function BookmarkIllustration(props: SvgProps) {
|
|||||||
d="M182.15 149.76H183.08l2.52-.07-.09.09v-1.83-1.06a1 1 0 01.87-.79h1.21a1 1 0 01.85 1v2.67l-.14-.15h3.18l-.15.15v-4-1a1 1 0 00-.44-.73l-1.39-1.38-1.34-1.32-.65-.64c-.23-.22-.39-.44-.64-.49a.85.85 0 00-.71.14c-.19.15-.4.39-.59.57l-1.19 1.08c-.72.74-1.48 1.43-2 2.06a2.15 2.15 0 00-.16 1.18v4.5a1.65 1.65 0 010-.38v-1.13c0-.44 0-1.14-.05-1.83v-1.14a2.36 2.36 0 01.17-1.31c.59-.73 1.29-1.36 2-2.14l1.13-1.16c.21-.2.37-.4.62-.61a1.14 1.14 0 01.95-.19 1.75 1.75 0 01.78.56l.65.64 1.35 1.32c.46.44.92.9 1.39 1.36a1.29 1.29 0 01.53.95v5.15h-3.48v-.15-2.67a.76.76 0 00-.62-.75h-1.15a.77.77 0 00-.67.6v2.92H182.49c-.28-.01-.35-.01-.34-.02z"
|
d="M182.15 149.76H183.08l2.52-.07-.09.09v-1.83-1.06a1 1 0 01.87-.79h1.21a1 1 0 01.85 1v2.67l-.14-.15h3.18l-.15.15v-4-1a1 1 0 00-.44-.73l-1.39-1.38-1.34-1.32-.65-.64c-.23-.22-.39-.44-.64-.49a.85.85 0 00-.71.14c-.19.15-.4.39-.59.57l-1.19 1.08c-.72.74-1.48 1.43-2 2.06a2.15 2.15 0 00-.16 1.18v4.5a1.65 1.65 0 010-.38v-1.13c0-.44 0-1.14-.05-1.83v-1.14a2.36 2.36 0 01.17-1.31c.59-.73 1.29-1.36 2-2.14l1.13-1.16c.21-.2.37-.4.62-.61a1.14 1.14 0 01.95-.19 1.75 1.75 0 01.78.56l.65.64 1.35 1.32c.46.44.92.9 1.39 1.36a1.29 1.29 0 01.53.95v5.15h-3.48v-.15-2.67a.76.76 0 00-.62-.75h-1.15a.77.77 0 00-.67.6v2.92H182.49c-.28-.01-.35-.01-.34-.02z"
|
||||||
fill="#263238"
|
fill="#263238"
|
||||||
/>
|
/>
|
||||||
<Rect x={197.63} y={138.21} width={116.21} height={13.34} rx={5.12} fill="#e0e0e0" />
|
<Rect fill="#e0e0e0" height={13.34} rx={5.12} width={116.21} x={197.63} y={138.21} />
|
||||||
<Path
|
<Path
|
||||||
d="M211.42 144.68V147h-1.17v-2.13c0-.66-.3-1-.82-1s-1 .35-1 1.09v2h-1.17v-5.57h1.17v2a1.79 1.79 0 011.28-.48 1.59 1.59 0 011.71 1.77zM215.08 146.79a1.61 1.61 0 01-1 .26 1.33 1.33 0 01-1.51-1.45v-1.66H212V143h.62v-1h1.17v1h1v.9h-1v1.65a.47.47 0 00.5.53.78.78 0 00.48-.15zM218.26 146.79a1.62 1.62 0 01-1 .26 1.33 1.33 0 01-1.5-1.45v-1.66h-.63V143h.63v-1H217v1h1v.9h-1v1.65a.47.47 0 00.5.53.75.75 0 00.47-.15zM223.32 145a2 2 0 01-2 2.08 1.57 1.57 0 01-1.22-.49v1.89h-1.17V143H220v.47a1.55 1.55 0 011.27-.53 2 2 0 012.05 2.06zm-1.19 0a1 1 0 10-1 1.12 1 1 0 001-1.12zM223.81 143.62a.73.73 0 011.46 0 .73.73 0 11-1.46 0zm0 2.69a.73.73 0 011.46 0 .73.73 0 11-1.46 0zM228.15 140.67h1l-2.48 7.07h-1zM230.55 140.67h1l-2.47 7.07h-1.05z"
|
d="M211.42 144.68V147h-1.17v-2.13c0-.66-.3-1-.82-1s-1 .35-1 1.09v2h-1.17v-5.57h1.17v2a1.79 1.79 0 011.28-.48 1.59 1.59 0 011.71 1.77zM215.08 146.79a1.61 1.61 0 01-1 .26 1.33 1.33 0 01-1.51-1.45v-1.66H212V143h.62v-1h1.17v1h1v.9h-1v1.65a.47.47 0 00.5.53.78.78 0 00.48-.15zM218.26 146.79a1.62 1.62 0 01-1 .26 1.33 1.33 0 01-1.5-1.45v-1.66h-.63V143h.63v-1H217v1h1v.9h-1v1.65a.47.47 0 00.5.53.75.75 0 00.47-.15zM223.32 145a2 2 0 01-2 2.08 1.57 1.57 0 01-1.22-.49v1.89h-1.17V143H220v.47a1.55 1.55 0 011.27-.53 2 2 0 012.05 2.06zm-1.19 0a1 1 0 10-1 1.12 1 1 0 001-1.12zM223.81 143.62a.73.73 0 011.46 0 .73.73 0 11-1.46 0zm0 2.69a.73.73 0 011.46 0 .73.73 0 11-1.46 0zM228.15 140.67h1l-2.48 7.07h-1zM230.55 140.67h1l-2.47 7.07h-1.05z"
|
||||||
fill="#263238"
|
fill="#263238"
|
||||||
@@ -137,8 +141,14 @@ export default function BookmarkIllustration(props: SvgProps) {
|
|||||||
d="M102.43 438.51a.79.79 0 010-.25v-.74c0-.68 0-1.63-.09-2.83-.08-2.51-.18-6.06-.3-10.43s-.32-9.64-.64-15.45-.76-12.2-1.64-18.85a61.07 61.07 0 00-1.89-9.58A50.15 50.15 0 0094.3 372a48.59 48.59 0 00-4.42-6.94 45.1 45.1 0 00-4.82-5.29 35.57 35.57 0 00-8.55-5.77c-.56-.26-1.05-.51-1.48-.68l-1.12-.44-.68-.27-.23-.11.24.06.71.23 1.13.39c.45.16.94.4 1.51.64a34.33 34.33 0 018.71 5.74 44.83 44.83 0 014.92 5.3 47.87 47.87 0 014.49 7 49.31 49.31 0 013.57 8.45 60.16 60.16 0 011.92 9.65c.88 6.68 1.3 13.07 1.6 18.89s.43 11.07.53 15.47.12 8 .13 10.44V438.33a.67.67 0 01-.03.18zM106.54 443.68a32.74 32.74 0 01-1.25-4.91 51.7 51.7 0 01-.52-13.78 92 92 0 014.35-20c2.35-7.35 5.53-15.21 8.25-23.66a109.27 109.27 0 003.28-12.52 110.19 110.19 0 001.62-11.94 173.75 173.75 0 00.36-20.42c-.19-5.82-.5-10.53-.7-13.78-.09-1.6-.16-2.85-.22-3.74v-1a1.37 1.37 0 010-.33 1.23 1.23 0 010 .33c0 .24.06.57.1 1 .07.89.18 2.14.31 3.74.26 3.25.62 7.95.86 13.77a166.42 166.42 0 01-.25 20.48 111.7 111.7 0 01-1.6 12 107.63 107.63 0 01-3.28 12.58c-2.73 8.47-5.93 16.32-8.29 23.64a93.11 93.11 0 00-4.43 19.86 53.33 53.33 0 00.36 13.71c.28 1.6.54 2.84.76 3.67.1.39.17.71.23 1a1.27 1.27 0 01.06.3z"
|
d="M102.43 438.51a.79.79 0 010-.25v-.74c0-.68 0-1.63-.09-2.83-.08-2.51-.18-6.06-.3-10.43s-.32-9.64-.64-15.45-.76-12.2-1.64-18.85a61.07 61.07 0 00-1.89-9.58A50.15 50.15 0 0094.3 372a48.59 48.59 0 00-4.42-6.94 45.1 45.1 0 00-4.82-5.29 35.57 35.57 0 00-8.55-5.77c-.56-.26-1.05-.51-1.48-.68l-1.12-.44-.68-.27-.23-.11.24.06.71.23 1.13.39c.45.16.94.4 1.51.64a34.33 34.33 0 018.71 5.74 44.83 44.83 0 014.92 5.3 47.87 47.87 0 014.49 7 49.31 49.31 0 013.57 8.45 60.16 60.16 0 011.92 9.65c.88 6.68 1.3 13.07 1.6 18.89s.43 11.07.53 15.47.12 8 .13 10.44V438.33a.67.67 0 01-.03.18zM106.54 443.68a32.74 32.74 0 01-1.25-4.91 51.7 51.7 0 01-.52-13.78 92 92 0 014.35-20c2.35-7.35 5.53-15.21 8.25-23.66a109.27 109.27 0 003.28-12.52 110.19 110.19 0 001.62-11.94 173.75 173.75 0 00.36-20.42c-.19-5.82-.5-10.53-.7-13.78-.09-1.6-.16-2.85-.22-3.74v-1a1.37 1.37 0 010-.33 1.23 1.23 0 010 .33c0 .24.06.57.1 1 .07.89.18 2.14.31 3.74.26 3.25.62 7.95.86 13.77a166.42 166.42 0 01-.25 20.48 111.7 111.7 0 01-1.6 12 107.63 107.63 0 01-3.28 12.58c-2.73 8.47-5.93 16.32-8.29 23.64a93.11 93.11 0 00-4.43 19.86 53.33 53.33 0 00.36 13.71c.28 1.6.54 2.84.76 3.67.1.39.17.71.23 1a1.27 1.27 0 01.06.3z"
|
||||||
fill="#263238"
|
fill="#263238"
|
||||||
/>
|
/>
|
||||||
<Path d="M134.87 448.84L81.23 448.84 80 438.51 134.87 438.51 134.87 448.84z" fill="#455a64" />
|
<Path
|
||||||
<Path d="M86.98 447.66L93.69 478.63 122.84 478.63 129.11 447.66 86.98 447.66z" fill="#455a64" />
|
d="M134.87 448.84L81.23 448.84 80 438.51 134.87 438.51 134.87 448.84z"
|
||||||
|
fill="#455a64"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
d="M86.98 447.66L93.69 478.63 122.84 478.63 129.11 447.66 86.98 447.66z"
|
||||||
|
fill="#455a64"
|
||||||
|
/>
|
||||||
<Path
|
<Path
|
||||||
d="M134.87 448.84c0 .14-11.8.22-26.34.18s-26.34-.19-26.34-.33 11.79-.22 26.34-.18 26.34.19 26.34.33z"
|
d="M134.87 448.84c0 .14-11.8.22-26.34.18s-26.34-.19-26.34-.33 11.79-.22 26.34-.18 26.34.19 26.34.33z"
|
||||||
fill="#455a64"
|
fill="#455a64"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const useFlattenedItems = <T>(data: PaginatedResult<T> | undefined | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.pages && Array.isArray(data.pages) && data.pages.length > 0) {
|
if (data.pages && Array.isArray(data.pages) && data.pages.length > 0) {
|
||||||
return data.pages.flatMap(page => page.items || []);
|
return data.pages.flatMap((page) => page.items || []);
|
||||||
} else if (data.items && Array.isArray(data.items)) {
|
} else if (data.items && Array.isArray(data.items)) {
|
||||||
return data.items;
|
return data.items;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { formatDistanceToNowStrict, Locale } from "date-fns";
|
import { formatDistanceToNowStrict, Locale } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const useRelativeTime = (
|
export const useRelativeTime = (
|
||||||
dateInput: string | Date | number | null | undefined,
|
dateInput: string | Date | number | null | undefined,
|
||||||
@@ -12,7 +11,7 @@ export const useRelativeTime = (
|
|||||||
roundingMethod?: "floor" | "ceil" | "round";
|
roundingMethod?: "floor" | "ceil" | "round";
|
||||||
includeSeconds?: boolean;
|
includeSeconds?: boolean;
|
||||||
},
|
},
|
||||||
updateInterval: number = 60000
|
updateInterval: number = 60000,
|
||||||
): string => {
|
): string => {
|
||||||
const [relativeTime, setRelativeTime] = useState("");
|
const [relativeTime, setRelativeTime] = useState("");
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ export const useRelativeTime = (
|
|||||||
const date = new Date(dateInput);
|
const date = new Date(dateInput);
|
||||||
|
|
||||||
// Check if the date is valid
|
// Check if the date is valid
|
||||||
if (isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
setRelativeTime("Invalid Date");
|
setRelativeTime("Invalid Date");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,8 +32,8 @@ export const useRelativeTime = (
|
|||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
// Default options if none provided, ensures suffix is added
|
// Default options if none provided, ensures suffix is added
|
||||||
const effectiveOptions = {
|
const effectiveOptions = {
|
||||||
locale: fr,
|
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
|
locale: fr,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { SplashScreen, useRouter } from "expo-router";
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter, SplashScreen } from "expo-router";
|
import { clearTokens, getAccessToken, getRefreshToken, setTokens } from "@/store/auth";
|
||||||
|
|
||||||
import { clearTokens, setTokens, getAccessToken, getRefreshToken } from "@/store/auth";
|
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
@@ -16,11 +15,11 @@ type AuthState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuthContext = createContext<AuthState>({
|
const AuthContext = createContext<AuthState>({
|
||||||
isReady: false,
|
accessToken: null,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
|
isReady: false,
|
||||||
login: () => {},
|
login: () => {},
|
||||||
logout: () => {},
|
logout: () => {},
|
||||||
accessToken: null,
|
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,7 +52,10 @@ export function AuthProvider({ children }: React.PropsWithChildren) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadTokens = async () => {
|
const loadTokens = async () => {
|
||||||
try {
|
try {
|
||||||
const [storedAccess, storedRefresh] = await Promise.all([getAccessToken(), getRefreshToken()]);
|
const [storedAccess, storedRefresh] = await Promise.all([
|
||||||
|
getAccessToken(),
|
||||||
|
getRefreshToken(),
|
||||||
|
]);
|
||||||
|
|
||||||
if (storedAccess && storedRefresh) {
|
if (storedAccess && storedRefresh) {
|
||||||
setAccessToken(storedAccess);
|
setAccessToken(storedAccess);
|
||||||
@@ -72,11 +74,11 @@ export function AuthProvider({ children }: React.PropsWithChildren) {
|
|||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isReady,
|
accessToken,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
isReady,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
refreshToken,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Inter_100Thin,
|
Inter_100Thin,
|
||||||
Inter_200ExtraLight,
|
Inter_200ExtraLight,
|
||||||
@@ -14,6 +11,8 @@ import {
|
|||||||
useFonts,
|
useFonts,
|
||||||
} from "@expo-google-fonts/inter";
|
} from "@expo-google-fonts/inter";
|
||||||
import { SplashScreen } from "expo-router";
|
import { SplashScreen } from "expo-router";
|
||||||
|
import type React from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import * as Network from "expo-network";
|
import * as Network from "expo-network";
|
||||||
import { NetworkStateEvent } from "expo-network";
|
import { NetworkStateEvent } from "expo-network";
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
type NetworkState = {
|
type NetworkState = {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
@@ -34,7 +33,7 @@ export const NetworkProvider = ({ children }: React.PropsWithChildren) => {
|
|||||||
subscribeToNetworkChanges();
|
subscribeToNetworkChanges();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
subscription && subscription.remove();
|
subscription?.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
export const queryClient = new QueryClient();
|
export const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export const AppIcon = (props: AppLogoProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
|
height={width}
|
||||||
|
marginBottom="$2"
|
||||||
|
objectFit="contain"
|
||||||
source={require("@/assets/images/logo.png")}
|
source={require("@/assets/images/logo.png")}
|
||||||
width={height}
|
width={height}
|
||||||
height={width}
|
|
||||||
objectFit="contain"
|
|
||||||
marginBottom="$2"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import { View } from "tamagui";
|
|||||||
import { Caption } from "@/ui/components/typography";
|
import { Caption } from "@/ui/components/typography";
|
||||||
|
|
||||||
export const LoadingView = () => (
|
export const LoadingView = () => (
|
||||||
<View flex={1} padding="$4" backgroundColor="$background" alignItems="center" justifyContent="center" gap="$4">
|
<View
|
||||||
|
alignItems="center"
|
||||||
|
backgroundColor="$background"
|
||||||
|
flex={1}
|
||||||
|
gap="$4"
|
||||||
|
justifyContent="center"
|
||||||
|
padding="$4"
|
||||||
|
>
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
<Caption>Chargement...</Caption>
|
<Caption>Chargement...</Caption>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Caption } from "@/ui/components/typography";
|
import { Caption } from "@/ui/components/typography";
|
||||||
|
|
||||||
type ArticleCategoryPillProps = {
|
type ArticleCategoryPillProps = {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { GetProps, Image, styled } from "tamagui";
|
import { GetProps, Image, styled } from "tamagui";
|
||||||
|
|
||||||
const StyledImage = styled(Image, {
|
const StyledImage = styled(Image, {
|
||||||
borderRadius: "$4",
|
|
||||||
backgroundColor: "$gray3",
|
backgroundColor: "$gray3",
|
||||||
|
borderRadius: "$4",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -15,5 +15,7 @@ type ArticleCoverImageProps = GetProps<typeof StyledImage> & {
|
|||||||
export const ArticleCoverImage = (props: ArticleCoverImageProps) => {
|
export const ArticleCoverImage = (props: ArticleCoverImageProps) => {
|
||||||
const { width, height, uri, ...rest } = props;
|
const { width, height, uri, ...rest } = props;
|
||||||
|
|
||||||
return <StyledImage source={{ uri, cache: "force-cache" }} width={width} height={height} {...rest} />;
|
return (
|
||||||
|
<StyledImage height={height} source={{ cache: "force-cache", uri }} width={width} {...rest} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const ArticleList: ArticleListComponent = (props: ArticleListProps) => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[horizontal, displayMode]
|
[horizontal, displayMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnEndReached = useCallback(async () => {
|
const handleOnEndReached = useCallback(async () => {
|
||||||
@@ -92,18 +92,18 @@ const ArticleList: ArticleListComponent = (props: ArticleListProps) => {
|
|||||||
<FlatList
|
<FlatList
|
||||||
{...rest}
|
{...rest}
|
||||||
data={data}
|
data={data}
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
ItemSeparatorComponent={horizontal ? HorizontalSeparator : VerticalSeparator}
|
|
||||||
horizontal={horizontal}
|
horizontal={horizontal}
|
||||||
showsHorizontalScrollIndicator={false}
|
ItemSeparatorComponent={horizontal ? HorizontalSeparator : VerticalSeparator}
|
||||||
initialNumToRender={5}
|
initialNumToRender={5}
|
||||||
onEndReachedThreshold={0.5}
|
keyExtractor={keyExtractor}
|
||||||
removeClippedSubviews={true}
|
|
||||||
onEndReached={handleOnEndReached}
|
|
||||||
refreshing={refreshing}
|
|
||||||
ListFooterComponent={infiniteScroll ? LoadingIndicator : undefined}
|
|
||||||
ListEmptyComponent={() => <Text>Pas d’articles disponibles pour le moment.</Text>}
|
ListEmptyComponent={() => <Text>Pas d’articles disponibles pour le moment.</Text>}
|
||||||
|
ListFooterComponent={infiniteScroll ? LoadingIndicator : undefined}
|
||||||
|
onEndReached={handleOnEndReached}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
refreshing={refreshing}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
renderItem={renderItem}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import { Card, XStack, YStack } from "tamagui";
|
import { Card, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
@@ -18,24 +16,24 @@ export const ArticleMagazineCard = (props: ArticleMagazineCardProps) => {
|
|||||||
const relativeTime = useRelativeTime(data.publishedAt);
|
const relativeTime = useRelativeTime(data.publishedAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card width="100%" backgroundColor="transparent" borderRadius="$4" padding={0}>
|
<Card backgroundColor="transparent" borderRadius="$4" padding={0} width="100%">
|
||||||
<Link href={`/(authed)/(tabs)/articles/${data.id}`}>
|
<Link href={`/(authed)/(tabs)/articles/${data.id}`}>
|
||||||
<XStack flexDirection="row" gap="$3" alignItems="center">
|
<XStack alignItems="center" flexDirection="row" gap="$3">
|
||||||
<YStack flex={1} gap="$2">
|
<YStack flex={1} gap="$2">
|
||||||
<Text numberOfLines={2} fontWeight="600" fontSize="$5">
|
<Text fontSize="$5" fontWeight="600" numberOfLines={2}>
|
||||||
{data.title}
|
{data.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="$3" numberOfLines={2} color="$colorHover">
|
<Text color="$colorHover" numberOfLines={2} size="$3">
|
||||||
{data.excerpt}
|
{data.excerpt}
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
{data.image && <ArticleCoverImage uri={data.image} width={120} height={90} />}
|
{data.image && <ArticleCoverImage height={90} uri={data.image} width={120} />}
|
||||||
</XStack>
|
</XStack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<YStack marginTop="$3">
|
<YStack marginTop="$3">
|
||||||
<XStack justifyContent="space-between" alignItems="center">
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
<SourceReferencePill data={data.source} />
|
<SourceReferencePill data={data.source} />
|
||||||
<Caption>{relativeTime}</Caption>
|
<Caption>{relativeTime}</Caption>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import { Card, XStack, YStack } from "tamagui";
|
import { Card, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
@@ -19,22 +17,20 @@ export const ArticleOverviewCard = (props: ArticleOverviewCardProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card backgroundColor="transparent">
|
<Card backgroundColor="transparent">
|
||||||
<Link href={`/(authed)/(tabs)/articles/${data.id}`} asChild>
|
<Link asChild href={`/(authed)/(tabs)/articles/${data.id}`}>
|
||||||
<>
|
{data.image && <ArticleCoverImage height={200} uri={data.image} width="100%" />}
|
||||||
{data.image && <ArticleCoverImage uri={data.image} width="100%" height={200} />}
|
<YStack gap="$2" marginTop="$2">
|
||||||
<YStack marginTop="$2" gap="$2">
|
<Text fontSize="$5" fontWeight="600" numberOfLines={2}>
|
||||||
<Text numberOfLines={2} fontWeight="600" fontSize="$5">
|
|
||||||
{data.title}
|
{data.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="$3" numberOfLines={2}>
|
<Text numberOfLines={2} size="$3">
|
||||||
{data.excerpt}
|
{data.excerpt}
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
</>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<YStack marginTop="$2">
|
<YStack marginTop="$2">
|
||||||
<XStack justifyContent="space-between" alignItems="center">
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
<SourceReferencePill data={data.source} />
|
<SourceReferencePill data={data.source} />
|
||||||
<Caption>{relativeTime}</Caption>
|
<Caption>{relativeTime}</Caption>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import ContentLoader, { Circle, Rect } from "react-content-loader/native";
|
import ContentLoader, { Circle, Rect } from "react-content-loader/native";
|
||||||
import { Dimensions, FlatList } from "react-native";
|
import { Dimensions, FlatList } from "react-native";
|
||||||
@@ -16,71 +16,71 @@ type ArticleSkeletonListProps = {
|
|||||||
|
|
||||||
const OverviewCardSkeleton = (props: any) => (
|
const OverviewCardSkeleton = (props: any) => (
|
||||||
<ContentLoader
|
<ContentLoader
|
||||||
speed={1}
|
animate={true}
|
||||||
interval={0.3}
|
|
||||||
backgroundColor="#D4D5D8"
|
backgroundColor="#D4D5D8"
|
||||||
foregroundColor="white"
|
foregroundColor="white"
|
||||||
height={350}
|
height={350}
|
||||||
animate={true}
|
interval={0.3}
|
||||||
|
speed={1}
|
||||||
width="100%"
|
width="100%"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Rect x="0" y="0" rx="8" ry="8" width="100%" height="200" />
|
<Rect height="200" rx="8" ry="8" width="100%" x="0" y="0" />
|
||||||
<Rect x="0" y="216" rx="4" ry="4" width="80%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="80%" x="0" y="216" />
|
||||||
<Rect x="0" y="232" rx="4" ry="4" width="100%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="100%" x="0" y="232" />
|
||||||
|
|
||||||
<Rect x="0" y="256" rx="4" ry="4" width="100%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="100%" x="0" y="256" />
|
||||||
<Rect x="0" y="272" rx="4" ry="4" width="60%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="60%" x="0" y="272" />
|
||||||
|
|
||||||
<Circle cx="10" cy="310" r="9" />
|
<Circle cx="10" cy="310" r="9" />
|
||||||
<Rect x="30" y="305" rx="4" ry="4" width="15%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="15%" x="30" y="305" />
|
||||||
<Rect x="215" y="305" rx="4" ry="4" width="20%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="20%" x="215" y="305" />
|
||||||
</ContentLoader>
|
</ContentLoader>
|
||||||
);
|
);
|
||||||
|
|
||||||
const MagazineCardSkeleton = (props: any) => (
|
const MagazineCardSkeleton = (props: any) => (
|
||||||
<ContentLoader
|
<ContentLoader
|
||||||
speed={1.5}
|
animate={true}
|
||||||
backgroundColor="#D4D5D8"
|
backgroundColor="#D4D5D8"
|
||||||
foregroundColor="white"
|
foregroundColor="white"
|
||||||
height={140}
|
height={140}
|
||||||
animate={true}
|
speed={1.5}
|
||||||
width="100%"
|
width="100%"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Rect x="235" y="0" rx="8" ry="8" width="120" height="90" />
|
<Rect height="90" rx="8" ry="8" width="120" x="235" y="0" />
|
||||||
|
|
||||||
<Rect x="0" y="0" rx="4" ry="4" width="54%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="54%" x="0" y="0" />
|
||||||
<Rect x="0" y="16" rx="4" ry="4" width="56%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="56%" x="0" y="16" />
|
||||||
<Rect x="0" y="40" rx="4" ry="4" width="55%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="55%" x="0" y="40" />
|
||||||
<Rect x="0" y="56" rx="4" ry="4" width="55%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="55%" x="0" y="56" />
|
||||||
<Rect x="0" y="72" rx="4" ry="4" width="55%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="55%" x="0" y="72" />
|
||||||
|
|
||||||
<Circle cx="10" cy="110" r="9" />
|
<Circle cx="10" cy="110" r="9" />
|
||||||
<Rect x="30" y="105" rx="4" ry="4" width="15%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="15%" x="30" y="105" />
|
||||||
<Rect x="315" y="105" rx="4" ry="4" width="40" height="10" />
|
<Rect height="10" rx="4" ry="4" width="40" x="315" y="105" />
|
||||||
</ContentLoader>
|
</ContentLoader>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TextOnlyCardSkeleton = (props: any) => (
|
const TextOnlyCardSkeleton = (props: any) => (
|
||||||
<ContentLoader
|
<ContentLoader
|
||||||
speed={1.5}
|
animate={true}
|
||||||
backgroundColor="#D4D5D8"
|
backgroundColor="#D4D5D8"
|
||||||
foregroundColor="white"
|
foregroundColor="white"
|
||||||
height={150}
|
height={150}
|
||||||
animate={true}
|
speed={1.5}
|
||||||
width="100%"
|
width="100%"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Rect x="0" y="16" rx="4" ry="4" width="80%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="80%" x="0" y="16" />
|
||||||
<Rect x="0" y="32" rx="4" ry="4" width="100%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="100%" x="0" y="32" />
|
||||||
|
|
||||||
<Rect x="0" y="56" rx="4" ry="4" width="100%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="100%" x="0" y="56" />
|
||||||
<Rect x="0" y="72" rx="4" ry="4" width="60%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="60%" x="0" y="72" />
|
||||||
|
|
||||||
<Circle cx="10" cy="110" r="9" />
|
<Circle cx="10" cy="110" r="9" />
|
||||||
<Rect x="30" y="105" rx="4" ry="4" width="15%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="15%" x="30" y="105" />
|
||||||
<Rect x="215" y="105" rx="4" ry="4" width="20%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="20%" x="215" y="105" />
|
||||||
</ContentLoader>
|
</ContentLoader>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,7 +100,9 @@ const selectSkeletonComponent = (displayMode: ArticleListDisplayMode) => {
|
|||||||
export const ArticleSkeletonList = (props: ArticleSkeletonListProps) => {
|
export const ArticleSkeletonList = (props: ArticleSkeletonListProps) => {
|
||||||
const { horizontal = false, displayMode = "magazine" } = props;
|
const { horizontal = false, displayMode = "magazine" } = props;
|
||||||
|
|
||||||
const ItemSeparator = horizontal ? ArticleList.HorizontalSeparator : ArticleList.VerticalSeparator;
|
const ItemSeparator = horizontal
|
||||||
|
? ArticleList.HorizontalSeparator
|
||||||
|
: ArticleList.VerticalSeparator;
|
||||||
|
|
||||||
const renderItem = useCallback(() => {
|
const renderItem = useCallback(() => {
|
||||||
const itemWidth = horizontal ? screenWidth * 0.7 : screenWidth;
|
const itemWidth = horizontal ? screenWidth * 0.7 : screenWidth;
|
||||||
@@ -115,15 +117,15 @@ export const ArticleSkeletonList = (props: ArticleSkeletonListProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data}
|
|
||||||
scrollEnabled={false}
|
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
ItemSeparatorComponent={ItemSeparator}
|
|
||||||
horizontal={horizontal}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={{ paddingBottom: 0 }}
|
contentContainerStyle={{ paddingBottom: 0 }}
|
||||||
|
data={data}
|
||||||
|
horizontal={horizontal}
|
||||||
|
ItemSeparatorComponent={ItemSeparator}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
|
renderItem={renderItem}
|
||||||
|
scrollEnabled={false}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import { Card, XStack, YStack } from "tamagui";
|
import { Card, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
@@ -17,14 +15,14 @@ export const ArticleTextOnlyCard = (props: ArticleTextOnlyCardProps) => {
|
|||||||
const relativeTime = useRelativeTime(data.publishedAt);
|
const relativeTime = useRelativeTime(data.publishedAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card width="100%" backgroundColor="transparent" borderRadius="$4" padding={0}>
|
<Card backgroundColor="transparent" borderRadius="$4" padding={0} width="100%">
|
||||||
<Link href={`/(authed)/(tabs)/articles/${data.id}`}>
|
<Link href={`/(authed)/(tabs)/articles/${data.id}`}>
|
||||||
<XStack flexDirection="row" gap="$3" alignItems="center">
|
<XStack alignItems="center" flexDirection="row" gap="$3">
|
||||||
<YStack flex={1} gap="$2">
|
<YStack flex={1} gap="$2">
|
||||||
<Text numberOfLines={2} fontWeight="600" fontSize="$5">
|
<Text fontSize="$5" fontWeight="600" numberOfLines={2}>
|
||||||
{data.title}
|
{data.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="$3" numberOfLines={2} color="$colorHover">
|
<Text color="$colorHover" numberOfLines={2} size="$3">
|
||||||
{data.excerpt}
|
{data.excerpt}
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
@@ -32,7 +30,7 @@ export const ArticleTextOnlyCard = (props: ArticleTextOnlyCardProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<YStack marginTop="$3">
|
<YStack marginTop="$3">
|
||||||
<XStack justifyContent="space-between" alignItems="center">
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
<SourceReferencePill data={data.source} />
|
<SourceReferencePill data={data.source} />
|
||||||
<Caption>{relativeTime}</Caption>
|
<Caption>{relativeTime}</Caption>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export { ArticleCategoryPill } from "@/ui/components/content/article/ArticleCategoryPill";
|
export { ArticleCategoryPill } from "@/ui/components/content/article/ArticleCategoryPill";
|
||||||
export { ArticleCoverImage } from "@/ui/components/content/article/ArticleCoverImage";
|
export { ArticleCoverImage } from "@/ui/components/content/article/ArticleCoverImage";
|
||||||
export { ArticleList } from "@/ui/components/content/article/ArticleList";
|
export { ArticleList } from "@/ui/components/content/article/ArticleList";
|
||||||
export { ArticleSkeletonList } from "@/ui/components/content/article/ArticleSkeleton";
|
|
||||||
export { ArticleMagazineCard } from "@/ui/components/content/article/ArticleMagazineCard";
|
export { ArticleMagazineCard } from "@/ui/components/content/article/ArticleMagazineCard";
|
||||||
export { ArticleOverviewCard } from "@/ui/components/content/article/ArticleOverviewCard";
|
export { ArticleOverviewCard } from "@/ui/components/content/article/ArticleOverviewCard";
|
||||||
|
export { ArticleSkeletonList } from "@/ui/components/content/article/ArticleSkeleton";
|
||||||
export { ArticleTextOnlyCard } from "@/ui/components/content/article/ArticleTextOnlyCard";
|
export { ArticleTextOnlyCard } from "@/ui/components/content/article/ArticleTextOnlyCard";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import { Card, XStack, YStack } from "tamagui";
|
import { Card, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
@@ -16,17 +14,17 @@ export const BookmarkCard = (props: BookmarkCardProps) => {
|
|||||||
const relativeTime = useRelativeTime(data.createdAt);
|
const relativeTime = useRelativeTime(data.createdAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card width="100%" backgroundColor="$gray7" borderRadius="$4" padding="$4">
|
<Card backgroundColor="$gray7" borderRadius="$4" padding="$4" width="100%">
|
||||||
<XStack gap="$4" justifyContent="space-between">
|
<XStack gap="$4" justifyContent="space-between">
|
||||||
<YStack>
|
<YStack>
|
||||||
<XStack flexDirection="row" gap="$3" alignItems="center">
|
<XStack alignItems="center" flexDirection="row" gap="$3">
|
||||||
<Link href={`/(authed)/(tabs)/bookmarks/${data.id}`}>
|
<Link href={`/(authed)/(tabs)/bookmarks/${data.id}`}>
|
||||||
<YStack flex={1} gap="$2">
|
<YStack flex={1} gap="$2">
|
||||||
<Text numberOfLines={2} fontWeight="600" fontSize="$5">
|
<Text fontSize="$5" fontWeight="600" numberOfLines={2}>
|
||||||
{data.name}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
{data.description && (
|
{data.description && (
|
||||||
<Text size="$3" numberOfLines={2} color="$colorHover">
|
<Text color="$colorHover" numberOfLines={2} size="$3">
|
||||||
{data.description}
|
{data.description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -35,7 +33,7 @@ export const BookmarkCard = (props: BookmarkCardProps) => {
|
|||||||
</XStack>
|
</XStack>
|
||||||
|
|
||||||
<YStack marginTop="$3">
|
<YStack marginTop="$3">
|
||||||
<XStack justifyContent="space-between" alignItems="center">
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
<Caption>{data.isPublic}</Caption>
|
<Caption>{data.isPublic}</Caption>
|
||||||
<Caption>{data.articlesCount} articles</Caption>
|
<Caption>{data.articlesCount} articles</Caption>
|
||||||
<Caption>{relativeTime}</Caption>
|
<Caption>{relativeTime}</Caption>
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { Heading, Text } from "@/ui/components/typography";
|
|||||||
|
|
||||||
export const BookmarkEmptyState = () => {
|
export const BookmarkEmptyState = () => {
|
||||||
return (
|
return (
|
||||||
<YStack flex={1} alignItems="center" justifyContent="center" gap="$2">
|
<YStack alignItems="center" flex={1} gap="$2" justifyContent="center">
|
||||||
<BookmarkIllustration width={250} height={250} />
|
<BookmarkIllustration height={250} width={250} />
|
||||||
<Heading alignSelf="center">Empty Bookmarks</Heading>
|
<Heading alignSelf="center">Empty Bookmarks</Heading>
|
||||||
<Text textAlign="center">Create a bookmark to save your favorite articles and access them later.</Text>
|
<Text textAlign="center">
|
||||||
|
Create a bookmark to save your favorite articles and access them later.
|
||||||
|
</Text>
|
||||||
|
|
||||||
<CreateBookmarkSheet />
|
<CreateBookmarkSheet />
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|||||||
@@ -37,7 +37,15 @@ const renderItem = ({ item }: { item: Bookmark }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BookmarkList: BookmarkListComponent = (props: BookmarkListProps) => {
|
const BookmarkList: BookmarkListComponent = (props: BookmarkListProps) => {
|
||||||
const { data, infiniteScroll = false, hasNextPage, isFetchingNextPage, fetchNextPage, refreshing, ...rest } = props;
|
const {
|
||||||
|
data,
|
||||||
|
infiniteScroll = false,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
refreshing,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
const handleOnEndReached = useCallback(async () => {
|
const handleOnEndReached = useCallback(async () => {
|
||||||
if (infiniteScroll && hasNextPage && !isFetchingNextPage && fetchNextPage) {
|
if (infiniteScroll && hasNextPage && !isFetchingNextPage && fetchNextPage) {
|
||||||
@@ -49,17 +57,17 @@ const BookmarkList: BookmarkListComponent = (props: BookmarkListProps) => {
|
|||||||
<FlatList
|
<FlatList
|
||||||
{...rest}
|
{...rest}
|
||||||
data={data}
|
data={data}
|
||||||
renderItem={renderItem}
|
|
||||||
onEndReached={handleOnEndReached}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
ItemSeparatorComponent={VerticalSeparator}
|
ItemSeparatorComponent={VerticalSeparator}
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
initialNumToRender={5}
|
initialNumToRender={5}
|
||||||
onEndReachedThreshold={0.5}
|
keyExtractor={keyExtractor}
|
||||||
removeClippedSubviews={true}
|
|
||||||
refreshing={refreshing}
|
|
||||||
ListEmptyComponent={<BookmarkEmptyState />}
|
ListEmptyComponent={<BookmarkEmptyState />}
|
||||||
ListFooterComponent={infiniteScroll && refreshing ? LoadingIndicator : undefined}
|
ListFooterComponent={infiniteScroll && refreshing ? LoadingIndicator : undefined}
|
||||||
|
onEndReached={handleOnEndReached}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
refreshing={refreshing}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
renderItem={renderItem}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { joiResolver } from "@hookform/resolvers/joi";
|
import { joiResolver } from "@hookform/resolvers/joi";
|
||||||
import { Sheet } from "@tamagui/sheet";
|
import { Sheet } from "@tamagui/sheet";
|
||||||
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { Button, YStack } from "tamagui";
|
import { Button, YStack } from "tamagui";
|
||||||
@@ -22,6 +21,13 @@ export const CreateBookmarkSheet = () => {
|
|||||||
|
|
||||||
const onSubmit = (data: BookmarkPayload) => {
|
const onSubmit = (data: BookmarkPayload) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
|
onError: (error: ErrorResponse) => {
|
||||||
|
Toast.show({
|
||||||
|
text1: "Erreur",
|
||||||
|
text2: safeMessage(error),
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
text1: "Félicitations !",
|
text1: "Félicitations !",
|
||||||
@@ -30,13 +36,6 @@ export const CreateBookmarkSheet = () => {
|
|||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorResponse) => {
|
|
||||||
Toast.show({
|
|
||||||
text1: "Erreur",
|
|
||||||
text2: safeMessage(error),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,14 +44,14 @@ export const CreateBookmarkSheet = () => {
|
|||||||
<Button onPress={() => setOpen(true)}>Ajouter un signet</Button>
|
<Button onPress={() => setOpen(true)}>Ajouter un signet</Button>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
modal={true}
|
animation="medium"
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
snapPointsMode="percent"
|
|
||||||
snapPoints={[65, 90]}
|
|
||||||
dismissOnOverlayPress={true}
|
dismissOnOverlayPress={true}
|
||||||
dismissOnSnapToBottom={true}
|
dismissOnSnapToBottom={true}
|
||||||
animation="medium"
|
modal={true}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
open={open}
|
||||||
|
snapPoints={[65, 90]}
|
||||||
|
snapPointsMode="percent"
|
||||||
>
|
>
|
||||||
<Sheet.Overlay
|
<Sheet.Overlay
|
||||||
animation="lazy"
|
animation="lazy"
|
||||||
@@ -61,41 +60,41 @@ export const CreateBookmarkSheet = () => {
|
|||||||
exitStyle={{ opacity: 0 }}
|
exitStyle={{ opacity: 0 }}
|
||||||
/>
|
/>
|
||||||
<Sheet.Frame
|
<Sheet.Frame
|
||||||
flex={1}
|
|
||||||
backgroundColor="$background"
|
|
||||||
padding="$4"
|
|
||||||
gap="$4"
|
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
backgroundColor="$background"
|
||||||
|
flex={1}
|
||||||
|
gap="$4"
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
|
padding="$4"
|
||||||
>
|
>
|
||||||
<YStack width="100%">
|
<YStack width="100%">
|
||||||
<Sheet.Handle theme="accent" />
|
<Sheet.Handle theme="accent" />
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
name="name"
|
caption="Enter a name for your bookmark."
|
||||||
control={control}
|
control={control}
|
||||||
label="Name"
|
label="Name"
|
||||||
caption="Enter a name for your bookmark."
|
name="name"
|
||||||
placeholder="My awesome bookmark"
|
placeholder="My awesome bookmark"
|
||||||
/>
|
/>
|
||||||
<FormTextArea
|
<FormTextArea
|
||||||
name="description"
|
|
||||||
control={control}
|
|
||||||
caption="Describe your bookmark for easy retrieval."
|
caption="Describe your bookmark for easy retrieval."
|
||||||
|
control={control}
|
||||||
label="Description"
|
label="Description"
|
||||||
|
name="description"
|
||||||
placeholder="A brief description..."
|
placeholder="A brief description..."
|
||||||
/>
|
/>
|
||||||
<FormSwitch
|
<FormSwitch
|
||||||
name="isPublic"
|
|
||||||
control={control}
|
control={control}
|
||||||
label="Public"
|
|
||||||
description="A public bookmark is visible and accessible to other users"
|
description="A public bookmark is visible and accessible to other users"
|
||||||
|
label="Public"
|
||||||
|
name="isPublic"
|
||||||
/>
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
label="Créer le signet"
|
|
||||||
onPress={handleSubmit(onSubmit)}
|
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
isValid={formState.isValid}
|
isValid={formState.isValid}
|
||||||
|
label="Créer le signet"
|
||||||
|
onPress={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
</Sheet.Frame>
|
</Sheet.Frame>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { joiResolver } from "@hookform/resolvers/joi";
|
import { joiResolver } from "@hookform/resolvers/joi";
|
||||||
import { Sheet } from "@tamagui/sheet";
|
import { Sheet } from "@tamagui/sheet";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { Button, YStack } from "tamagui";
|
import { Button, YStack } from "tamagui";
|
||||||
|
|
||||||
import { useUpdateBookmark } from "@/api/request/feed-management/bookmark";
|
import { useUpdateBookmark } from "@/api/request/feed-management/bookmark";
|
||||||
import { Bookmark, BookmarkPayload, BookmarkPayloadSchema } from "@/api/schema/feed-management/bookmark";
|
import {
|
||||||
|
Bookmark,
|
||||||
|
BookmarkPayload,
|
||||||
|
BookmarkPayloadSchema,
|
||||||
|
} from "@/api/schema/feed-management/bookmark";
|
||||||
import { ErrorResponse, safeMessage } from "@/api/shared";
|
import { ErrorResponse, safeMessage } from "@/api/shared";
|
||||||
import { FormSwitch } from "@/ui/components/controls/forms/Switch";
|
import { FormSwitch } from "@/ui/components/controls/forms/Switch";
|
||||||
import { FormTextArea } from "@/ui/components/controls/forms/TextArea";
|
import { FormTextArea } from "@/ui/components/controls/forms/TextArea";
|
||||||
@@ -33,6 +36,13 @@ export const UpdateBookmarkSheet = (props: UpdateBookmarkSheetProps) => {
|
|||||||
|
|
||||||
const onSubmit = (data: BookmarkPayload) => {
|
const onSubmit = (data: BookmarkPayload) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
|
onError: (error: ErrorResponse) => {
|
||||||
|
Toast.show({
|
||||||
|
text1: "Erreur",
|
||||||
|
text2: safeMessage(error),
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
text1: "Félicitations !",
|
text1: "Félicitations !",
|
||||||
@@ -41,13 +51,6 @@ export const UpdateBookmarkSheet = (props: UpdateBookmarkSheetProps) => {
|
|||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorResponse) => {
|
|
||||||
Toast.show({
|
|
||||||
text1: "Erreur",
|
|
||||||
text2: safeMessage(error),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,14 +59,14 @@ export const UpdateBookmarkSheet = (props: UpdateBookmarkSheetProps) => {
|
|||||||
<Button onPress={() => setOpen(true)}>Modifier</Button>
|
<Button onPress={() => setOpen(true)}>Modifier</Button>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
modal={true}
|
animation="medium"
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
snapPointsMode="percent"
|
|
||||||
snapPoints={[65, 90]}
|
|
||||||
dismissOnOverlayPress={true}
|
dismissOnOverlayPress={true}
|
||||||
dismissOnSnapToBottom={true}
|
dismissOnSnapToBottom={true}
|
||||||
animation="medium"
|
modal={true}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
open={open}
|
||||||
|
snapPoints={[65, 90]}
|
||||||
|
snapPointsMode="percent"
|
||||||
>
|
>
|
||||||
<Sheet.Overlay
|
<Sheet.Overlay
|
||||||
animation="lazy"
|
animation="lazy"
|
||||||
@@ -72,41 +75,41 @@ export const UpdateBookmarkSheet = (props: UpdateBookmarkSheetProps) => {
|
|||||||
exitStyle={{ opacity: 0 }}
|
exitStyle={{ opacity: 0 }}
|
||||||
/>
|
/>
|
||||||
<Sheet.Frame
|
<Sheet.Frame
|
||||||
flex={1}
|
|
||||||
backgroundColor="$background"
|
|
||||||
padding="$4"
|
|
||||||
gap="$4"
|
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
backgroundColor="$background"
|
||||||
|
flex={1}
|
||||||
|
gap="$4"
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
|
padding="$4"
|
||||||
>
|
>
|
||||||
<YStack width="100%">
|
<YStack width="100%">
|
||||||
<Sheet.Handle theme="accent" />
|
<Sheet.Handle theme="accent" />
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
name="name"
|
caption="Enter a name for your bookmark."
|
||||||
control={control}
|
control={control}
|
||||||
label="Name"
|
label="Name"
|
||||||
caption="Enter a name for your bookmark."
|
name="name"
|
||||||
placeholder="My awesome bookmark"
|
placeholder="My awesome bookmark"
|
||||||
/>
|
/>
|
||||||
<FormTextArea
|
<FormTextArea
|
||||||
name="description"
|
|
||||||
control={control}
|
|
||||||
caption="Describe your bookmark for easy retrieval."
|
caption="Describe your bookmark for easy retrieval."
|
||||||
|
control={control}
|
||||||
label="Description"
|
label="Description"
|
||||||
|
name="description"
|
||||||
placeholder="A brief description..."
|
placeholder="A brief description..."
|
||||||
/>
|
/>
|
||||||
<FormSwitch
|
<FormSwitch
|
||||||
name="isPublic"
|
|
||||||
control={control}
|
control={control}
|
||||||
label="Public"
|
|
||||||
description="A public bookmark is visible and accessible to other users"
|
description="A public bookmark is visible and accessible to other users"
|
||||||
|
label="Public"
|
||||||
|
name="isPublic"
|
||||||
/>
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
label="Modifier le signet"
|
|
||||||
onPress={handleSubmit(onSubmit)}
|
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
isValid={formState.isValid}
|
isValid={formState.isValid}
|
||||||
|
label="Modifier le signet"
|
||||||
|
onPress={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
</Sheet.Frame>
|
</Sheet.Frame>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -25,35 +25,35 @@ export const SourceFollowButton = (props: SourceFollowButtonProps) => {
|
|||||||
`Êtes-vous sûr de vouloir ne plus suivre ${name} ?`,
|
`Êtes-vous sûr de vouloir ne plus suivre ${name} ?`,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: "Annuler",
|
|
||||||
style: "cancel",
|
style: "cancel",
|
||||||
|
text: "Annuler",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Ne plus suivre",
|
|
||||||
style: "destructive",
|
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
unfollow();
|
unfollow();
|
||||||
setIsFollowed(prev => !prev);
|
setIsFollowed((prev) => !prev);
|
||||||
},
|
},
|
||||||
|
style: "destructive",
|
||||||
|
text: "Ne plus suivre",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ cancelable: false }
|
{ cancelable: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
follow();
|
follow();
|
||||||
setIsFollowed(prev => !prev);
|
setIsFollowed((prev) => !prev);
|
||||||
}
|
}
|
||||||
}, [isFollowed, name, unfollow, follow, setIsFollowed]);
|
}, [isFollowed, name, unfollow, follow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="$2"
|
|
||||||
theme={isFollowed ? "alt1" : "surface1"}
|
|
||||||
chromeless={isFollowed}
|
chromeless={isFollowed}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onPress={handlePress}
|
|
||||||
minWidth={80}
|
minWidth={80}
|
||||||
|
onPress={handlePress}
|
||||||
paddingHorizontal="$2"
|
paddingHorizontal="$2"
|
||||||
|
size="$2"
|
||||||
|
theme={isFollowed ? "alt1" : "surface1"}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{loading ? <ActivityIndicator /> : isFollowed ? "Suivi" : "Suivre"}
|
{loading ? <ActivityIndicator /> : isFollowed ? "Suivi" : "Suivre"}
|
||||||
|
|||||||
@@ -29,22 +29,22 @@ const SourceList: SourceOverviewListComponent = (props: SourceOverviewListProps)
|
|||||||
({ item }: { item: SourceOverview }) => {
|
({ item }: { item: SourceOverview }) => {
|
||||||
return <SourceOverviewCard data={item} horizontal={horizontal} />;
|
return <SourceOverviewCard data={item} horizontal={horizontal} />;
|
||||||
},
|
},
|
||||||
[horizontal]
|
[horizontal],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
{...rest}
|
{...rest}
|
||||||
data={data}
|
data={data}
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
ItemSeparatorComponent={horizontal ? HorizontalSeparator : VerticalSeparator}
|
|
||||||
horizontal={horizontal}
|
horizontal={horizontal}
|
||||||
showsHorizontalScrollIndicator={false}
|
ItemSeparatorComponent={horizontal ? HorizontalSeparator : VerticalSeparator}
|
||||||
initialNumToRender={5}
|
initialNumToRender={5}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
ListEmptyComponent={() => <Paragraph>Pas de sources disponibles pour le moment.</Paragraph>}
|
||||||
onEndReachedThreshold={0.5}
|
onEndReachedThreshold={0.5}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
ListEmptyComponent={() => <Paragraph>Pas de sources disponibles pour le moment.</Paragraph>}
|
renderItem={renderItem}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ import { Text } from "@/ui/components/typography";
|
|||||||
|
|
||||||
const SourceCardFrame = styled(YStack, {
|
const SourceCardFrame = styled(YStack, {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "$2",
|
|
||||||
borderRadius: "$4",
|
borderRadius: "$4",
|
||||||
|
gap: "$2",
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
horizontal: {
|
horizontal: {
|
||||||
true: {
|
|
||||||
maxWidth: 100,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
false: {
|
false: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
|
||||||
width: "100%",
|
|
||||||
gap: "$4",
|
gap: "$4",
|
||||||
|
justifyContent: "space-between",
|
||||||
paddingVertical: "$2",
|
paddingVertical: "$2",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
true: {
|
||||||
|
flexShrink: 0,
|
||||||
|
maxWidth: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -41,17 +41,17 @@ export const SourceOverviewCard = (props: SourceCardProps) => {
|
|||||||
return (
|
return (
|
||||||
<SourceCardFrame horizontal={horizontal} {...rest}>
|
<SourceCardFrame horizontal={horizontal} {...rest}>
|
||||||
<Link href={`/(authed)/(tabs)/sources/${data.name}`}>
|
<Link href={`/(authed)/(tabs)/sources/${data.name}`}>
|
||||||
<SourceProfileImage name={data.name} image={data.image} size={horizontal ? 65 : 50} />
|
<SourceProfileImage image={data.image} name={data.name} size={horizontal ? 65 : 50} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href={`/(authed)/(tabs)/sources/${data.name}`} asChild>
|
<Link asChild href={`/(authed)/(tabs)/sources/${data.name}`}>
|
||||||
{horizontal ? (
|
{horizontal ? (
|
||||||
<Text
|
<Text
|
||||||
fontSize={nameFontSize}
|
fontSize={nameFontSize}
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
|
maxWidth="100%"
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
maxWidth="100%"
|
|
||||||
>
|
>
|
||||||
{data.displayName ?? data.name}
|
{data.displayName ?? data.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -70,7 +70,11 @@ export const SourceOverviewCard = (props: SourceCardProps) => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<SourceFollowButton id={data.id} name={data.displayName ?? data.name} followed={data.followed} />
|
<SourceFollowButton
|
||||||
|
followed={data.followed}
|
||||||
|
id={data.id}
|
||||||
|
name={data.displayName ?? data.name}
|
||||||
|
/>
|
||||||
</SourceCardFrame>
|
</SourceCardFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { GetProps, Image, styled } from "tamagui";
|
import { GetProps, Image, styled } from "tamagui";
|
||||||
|
|
||||||
const StyledImage = styled(Image, {
|
const StyledImage = styled(Image, {
|
||||||
borderRadius: "$12",
|
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
|
borderRadius: "$12",
|
||||||
});
|
});
|
||||||
|
|
||||||
type SourceAvatarProps = GetProps<typeof StyledImage> & {
|
type SourceAvatarProps = GetProps<typeof StyledImage> & {
|
||||||
@@ -19,13 +17,13 @@ export const SourceProfileImage = (props: SourceAvatarProps) => {
|
|||||||
return (
|
return (
|
||||||
<StyledImage
|
<StyledImage
|
||||||
accessibilityLabel={name}
|
accessibilityLabel={name}
|
||||||
source={{
|
|
||||||
uri: image,
|
|
||||||
cache: "force-cache",
|
|
||||||
}}
|
|
||||||
objectFit="contain"
|
|
||||||
width={size}
|
|
||||||
height={size}
|
height={size}
|
||||||
|
objectFit="contain"
|
||||||
|
source={{
|
||||||
|
cache: "force-cache",
|
||||||
|
uri: image,
|
||||||
|
}}
|
||||||
|
width={size}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import { Avatar, GetProps, XStack } from "tamagui";
|
import { Avatar, GetProps, XStack } from "tamagui";
|
||||||
|
|
||||||
@@ -19,16 +17,16 @@ export function SourceReferencePill(props: SourceReferencePillProps) {
|
|||||||
<Avatar circular size="$1">
|
<Avatar circular size="$1">
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
accessibilityLabel={data.name}
|
accessibilityLabel={data.name}
|
||||||
objectFit="contain"
|
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
|
objectFit="contain"
|
||||||
source={{
|
source={{
|
||||||
uri: data.image,
|
|
||||||
cache: "force-cache",
|
cache: "force-cache",
|
||||||
|
uri: data.image,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback backgroundColor="$gray10" />
|
<Avatar.Fallback backgroundColor="$gray10" />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Text size="$2" fontWeight="bold">
|
<Text fontWeight="bold" size="$2">
|
||||||
{data.displayName ?? data.name}
|
{data.displayName ?? data.name}
|
||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import ContentLoader, { Circle, Rect } from "react-content-loader/native";
|
import ContentLoader, { Circle, Rect } from "react-content-loader/native";
|
||||||
import { FlatList } from "react-native";
|
import { FlatList } from "react-native";
|
||||||
@@ -14,34 +14,34 @@ type SourceSkeletonListProps = {
|
|||||||
|
|
||||||
const VerticalSkeleton = (props: any) => (
|
const VerticalSkeleton = (props: any) => (
|
||||||
<ContentLoader
|
<ContentLoader
|
||||||
speed={1.5}
|
animate={true}
|
||||||
backgroundColor="#D4D5D8"
|
backgroundColor="#D4D5D8"
|
||||||
foregroundColor="white"
|
foregroundColor="white"
|
||||||
height={70}
|
height={70}
|
||||||
animate={true}
|
speed={1.5}
|
||||||
width="100%"
|
width="100%"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Circle cx="25" cy="30" r="25" />
|
<Circle cx="25" cy="30" r="25" />
|
||||||
<Rect x="70" y="10" rx="4" ry="4" width="25%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="25%" x="70" y="10" />
|
||||||
<Rect x="70" y="40" rx="4" ry="4" width="45%" height="10" />
|
<Rect height="10" rx="4" ry="4" width="45%" x="70" y="40" />
|
||||||
<Rect x="280" y="15" rx="4" ry="4" width="20%" height="30" />
|
<Rect height="30" rx="4" ry="4" width="20%" x="280" y="15" />
|
||||||
</ContentLoader>
|
</ContentLoader>
|
||||||
);
|
);
|
||||||
|
|
||||||
const HorizontalSkeleton = (props: any) => (
|
const HorizontalSkeleton = (props: any) => (
|
||||||
<ContentLoader
|
<ContentLoader
|
||||||
speed={1.5}
|
animate={true}
|
||||||
backgroundColor="#D4D5D8"
|
backgroundColor="#D4D5D8"
|
||||||
foregroundColor="white"
|
foregroundColor="white"
|
||||||
height={180}
|
height={180}
|
||||||
animate={true}
|
speed={1.5}
|
||||||
width={110}
|
width={110}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Circle cx="60" cy="40" r="33" />
|
<Circle cx="60" cy="40" r="33" />
|
||||||
<Rect x="10" y="85" rx="4" ry="4" width="100" height="10" />
|
<Rect height="10" rx="4" ry="4" width="100" x="10" y="85" />
|
||||||
<Rect x="25" y="105" rx="8" ry="8" width="70" height="25" />
|
<Rect height="25" rx="8" ry="8" width="70" x="25" y="105" />
|
||||||
</ContentLoader>
|
</ContentLoader>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -68,15 +68,15 @@ export const SourceSkeletonList = (props: SourceSkeletonListProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data}
|
|
||||||
scrollEnabled={false}
|
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
ItemSeparatorComponent={ItemSeparator}
|
|
||||||
horizontal={horizontal}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={{ paddingBottom: 0 }}
|
contentContainerStyle={{ paddingBottom: 0 }}
|
||||||
|
data={data}
|
||||||
|
horizontal={horizontal}
|
||||||
|
ItemSeparatorComponent={ItemSeparator}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
|
renderItem={renderItem}
|
||||||
|
scrollEnabled={false}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export { SourceList } from "@/ui/components/content/source/SourceList";
|
|
||||||
export { SourceFollowButton } from "@/ui/components/content/source/SourceFollowButton";
|
export { SourceFollowButton } from "@/ui/components/content/source/SourceFollowButton";
|
||||||
|
export { SourceList } from "@/ui/components/content/source/SourceList";
|
||||||
|
export { SourceOverviewCard } from "@/ui/components/content/source/SourceOverviewCard";
|
||||||
|
export { SourceProfileImage } from "@/ui/components/content/source/SourceProfileImage";
|
||||||
export { SourceReferencePill } from "@/ui/components/content/source/SourceReferencePill";
|
export { SourceReferencePill } from "@/ui/components/content/source/SourceReferencePill";
|
||||||
export { SourceSkeletonList } from "@/ui/components/content/source/SourceSkeleton";
|
export { SourceSkeletonList } from "@/ui/components/content/source/SourceSkeleton";
|
||||||
export { SourceProfileImage } from "@/ui/components/content/source/SourceProfileImage";
|
|
||||||
export { SourceOverviewCard } from "@/ui/components/content/source/SourceOverviewCard";
|
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ export const BackButton = (props: BackButtonProps & ButtonProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
chromeless
|
|
||||||
alignSelf="flex-start"
|
alignSelf="flex-start"
|
||||||
size="$4"
|
|
||||||
width="$4"
|
|
||||||
height="$4"
|
|
||||||
borderRadius="$12"
|
borderRadius="$12"
|
||||||
// backgroundColor="$gray6"
|
chromeless
|
||||||
|
height="$4"
|
||||||
icon={<ArrowLeft size="$1" />}
|
icon={<ArrowLeft size="$1" />}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
// backgroundColor="$gray6"
|
||||||
|
size="$4"
|
||||||
|
width="$4"
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export const IconButton = (props: IconButtonProps & ButtonProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
chromeless
|
|
||||||
alignSelf="flex-start"
|
alignSelf="flex-start"
|
||||||
|
borderRadius="$12"
|
||||||
|
chromeless
|
||||||
|
height="$4"
|
||||||
|
onPress={onPress}
|
||||||
size="$4"
|
size="$4"
|
||||||
width="$4"
|
width="$4"
|
||||||
height="$4"
|
|
||||||
borderRadius="$12"
|
|
||||||
onPress={onPress}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const SubmitButton = (props: SubmitButtonProps) => {
|
|||||||
return (
|
return (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
theme={!isValid || isPending ? "disabled" : "accent"}
|
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
|
theme={!isValid || isPending ? "disabled" : "accent"}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{isPending ? <ActivityIndicator /> : label}
|
{isPending ? <ActivityIndicator /> : label}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ export const EmailInput = (props: InputProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
label={label}
|
|
||||||
caption={caption}
|
|
||||||
error={error}
|
|
||||||
leadingAdornment={Mail}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
|
caption={caption}
|
||||||
|
error={error}
|
||||||
|
keyboardType="email-address"
|
||||||
|
label={label}
|
||||||
|
leadingAdornment={Mail}
|
||||||
|
onChangeText={onChangeText}
|
||||||
placeholder="votre@email.com..."
|
placeholder="votre@email.com..."
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
|
|
||||||
import { IconProps } from "@tamagui/helpers-icon";
|
import { IconProps } from "@tamagui/helpers-icon";
|
||||||
import { ColorTokens, GetProps, Input as TamaguiInput, Label, SizeTokens, styled, XStack, YStack } from "tamagui";
|
import React, { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
ColorTokens,
|
||||||
|
GetProps,
|
||||||
|
Label,
|
||||||
|
SizeTokens,
|
||||||
|
styled,
|
||||||
|
Input as TamaguiInput,
|
||||||
|
XStack,
|
||||||
|
YStack,
|
||||||
|
} from "tamagui";
|
||||||
|
|
||||||
import { Caption } from "@/ui/components/typography";
|
import { Caption } from "@/ui/components/typography";
|
||||||
|
|
||||||
const StyledInput = styled(TamaguiInput, {
|
const StyledInput = styled(TamaguiInput, {
|
||||||
size: "$large",
|
|
||||||
flex: 1,
|
|
||||||
borderWidth: 0,
|
|
||||||
placeholderTextColor: "$gray8",
|
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
|
borderWidth: 0,
|
||||||
|
flex: 1,
|
||||||
|
placeholderTextColor: "$gray8",
|
||||||
|
size: "$large",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type InputProps = GetProps<typeof StyledInput> & {
|
export type InputProps = GetProps<typeof StyledInput> & {
|
||||||
@@ -24,15 +32,16 @@ export type InputProps = GetProps<typeof StyledInput> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Input = (props: InputProps) => {
|
export const Input = (props: InputProps) => {
|
||||||
const { label, caption, error, leadingAdornment, trailingAdornment, onChangeText, id, ...rest } = props;
|
const { label, caption, error, leadingAdornment, trailingAdornment, onChangeText, id, ...rest } =
|
||||||
|
props;
|
||||||
|
|
||||||
const isInvalid = !!error;
|
const isInvalid = !!error;
|
||||||
const leadingAdornmentComponent = useMemo(() => {
|
const leadingAdornmentComponent = useMemo(() => {
|
||||||
return leadingAdornment ? (
|
return leadingAdornment ? (
|
||||||
<XStack paddingLeft="$3" style={{ justifyContent: "center", alignItems: "center" }}>
|
<XStack paddingLeft="$3" style={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
{React.createElement(leadingAdornment, {
|
{React.createElement(leadingAdornment, {
|
||||||
size: "$1",
|
|
||||||
color: "$gray9",
|
color: "$gray9",
|
||||||
|
size: "$1",
|
||||||
})}
|
})}
|
||||||
</XStack>
|
</XStack>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
@@ -42,17 +51,17 @@ export const Input = (props: InputProps) => {
|
|||||||
<YStack gap="$1">
|
<YStack gap="$1">
|
||||||
<YStack>
|
<YStack>
|
||||||
{label && (
|
{label && (
|
||||||
<Label htmlFor={id} fontWeight="bold" color={isInvalid ? "$red9" : undefined}>
|
<Label color={isInvalid ? "$red9" : undefined} fontWeight="bold" htmlFor={id}>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<XStack
|
<XStack
|
||||||
backgroundColor="$gray4"
|
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
backgroundColor="$gray4"
|
||||||
|
borderColor={isInvalid ? "$red9" : "transparent"}
|
||||||
borderRadius="$4"
|
borderRadius="$4"
|
||||||
borderWidth="$0.5"
|
borderWidth="$0.5"
|
||||||
borderColor={isInvalid ? "$red9" : "transparent"}
|
|
||||||
focusStyle={{
|
focusStyle={{
|
||||||
borderColor: "$accent8",
|
borderColor: "$accent8",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { Eye, EyeOff, Lock } from "@tamagui/lucide-icons";
|
import { Eye, EyeOff, Lock } from "@tamagui/lucide-icons";
|
||||||
|
import { useState } from "react";
|
||||||
import { XStack } from "tamagui";
|
import { XStack } from "tamagui";
|
||||||
|
|
||||||
import { Input, InputProps } from "@/ui/components/controls/forms/Input";
|
import { Input, InputProps } from "@/ui/components/controls/forms/Input";
|
||||||
@@ -12,21 +11,21 @@ export const PasswordInput = (props: InputProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
label={label}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
caption={caption}
|
caption={caption}
|
||||||
error={error}
|
error={error}
|
||||||
|
label={label}
|
||||||
leadingAdornment={Lock}
|
leadingAdornment={Lock}
|
||||||
secureTextEntry={!showPassword}
|
onChangeText={onChangeText}
|
||||||
paddingRight="$6"
|
paddingRight="$6"
|
||||||
placeholder="Mot de passe"
|
placeholder="Mot de passe"
|
||||||
|
secureTextEntry={!showPassword}
|
||||||
trailingAdornment={
|
trailingAdornment={
|
||||||
<XStack
|
<XStack
|
||||||
paddingRight="$3"
|
hitSlop={{ bottom: 10, left: 10, right: 10, top: 10 }}
|
||||||
onPress={() => setShowPassword(!showPassword)}
|
onPress={() => setShowPassword(!showPassword)}
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
paddingRight="$3"
|
||||||
>
|
>
|
||||||
{showPassword ? <Eye size="$1" color="$gray9" /> : <EyeOff size="$1" color="$gray9" />}
|
{showPassword ? <Eye color="$gray9" size="$1" /> : <EyeOff color="$gray9" size="$1" />}
|
||||||
</XStack>
|
</XStack>
|
||||||
}
|
}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ export const Switch = (props: SwitchProps) => {
|
|||||||
</Label>
|
</Label>
|
||||||
{description && <Caption>{description}</Caption>}
|
{description && <Caption>{description}</Caption>}
|
||||||
</YStack>
|
</YStack>
|
||||||
<TamaguiSwitch id={id} checked={isChecked} onCheckedChange={onCheckedChange} size="$3" {...rest}>
|
<TamaguiSwitch
|
||||||
|
checked={isChecked}
|
||||||
|
id={id}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
size="$3"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
<TamaguiSwitch.Thumb animation="bouncy" />
|
<TamaguiSwitch.Thumb animation="bouncy" />
|
||||||
</TamaguiSwitch>
|
</TamaguiSwitch>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { withController } from "@/ui/components/controls/forms/withController";
|
|||||||
import { Caption } from "@/ui/components/typography";
|
import { Caption } from "@/ui/components/typography";
|
||||||
|
|
||||||
const StyledTextArea = styled(TamaguiTextArea, {
|
const StyledTextArea = styled(TamaguiTextArea, {
|
||||||
size: "$4",
|
backgroundColor: "transparent",
|
||||||
|
borderWidth: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 100,
|
minHeight: 100,
|
||||||
borderWidth: 0,
|
|
||||||
placeholderTextColor: "$gray8",
|
placeholderTextColor: "$gray8",
|
||||||
backgroundColor: "transparent",
|
size: "$4",
|
||||||
});
|
});
|
||||||
|
|
||||||
type TextAreaProps = GetProps<typeof StyledTextArea> & {
|
type TextAreaProps = GetProps<typeof StyledTextArea> & {
|
||||||
@@ -28,7 +28,7 @@ export const TextArea = (props: TextAreaProps) => {
|
|||||||
return (
|
return (
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
{label && (
|
{label && (
|
||||||
<Label htmlFor={id} fontWeight="bold" color={isInvalid ? "$red9" : undefined}>
|
<Label color={isInvalid ? "$red9" : undefined} fontWeight="bold" htmlFor={id}>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
@@ -36,9 +36,9 @@ export const TextArea = (props: TextAreaProps) => {
|
|||||||
<XStack
|
<XStack
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
backgroundColor="$gray4"
|
backgroundColor="$gray4"
|
||||||
|
borderColor={isInvalid ? "$red9" : "transparent"}
|
||||||
borderRadius="$4"
|
borderRadius="$4"
|
||||||
borderWidth="$0.5"
|
borderWidth="$0.5"
|
||||||
borderColor={isInvalid ? "$red9" : "transparent"}
|
|
||||||
focusStyle={{
|
focusStyle={{
|
||||||
borderColor: "$accent8",
|
borderColor: "$accent8",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ export const TextInput = (props: InputProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
label={label}
|
|
||||||
caption={caption}
|
caption={caption}
|
||||||
error={error}
|
error={error}
|
||||||
|
label={label}
|
||||||
leadingAdornment={leadingAdornment}
|
leadingAdornment={leadingAdornment}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { EmailInput, FormEmailInput } from "@/ui/components/controls/forms/EmailInput";
|
export { EmailInput, FormEmailInput } from "@/ui/components/controls/forms/EmailInput";
|
||||||
export { TextInput, FormTextInput } from "@/ui/components/controls/forms/TextInput";
|
export { FormPasswordInput, PasswordInput } from "@/ui/components/controls/forms/PasswordInput";
|
||||||
export { PasswordInput, FormPasswordInput } from "@/ui/components/controls/forms/PasswordInput";
|
export { FormSwitch, Switch } from "@/ui/components/controls/forms/Switch";
|
||||||
export { Switch, FormSwitch } from "@/ui/components/controls/forms/Switch";
|
export { FormTextArea, TextArea } from "@/ui/components/controls/forms/TextArea";
|
||||||
export { TextArea, FormTextArea } from "@/ui/components/controls/forms/TextArea";
|
export { FormTextInput, TextInput } from "@/ui/components/controls/forms/TextInput";
|
||||||
|
|||||||
@@ -15,14 +15,16 @@ type ControllerWrapperProps<T> = {
|
|||||||
control: ControllerProps<any, any, any>["control"];
|
control: ControllerProps<any, any, any>["control"];
|
||||||
} & Omit<T, keyof WithControllerProps>;
|
} & Omit<T, keyof WithControllerProps>;
|
||||||
|
|
||||||
export const withController = <T extends WithControllerProps>(Component: React.ComponentType<T>) => {
|
export const withController = <T extends WithControllerProps>(
|
||||||
|
Component: React.ComponentType<T>,
|
||||||
|
) => {
|
||||||
const ControllerWrapper = (props: ControllerWrapperProps<T>) => {
|
const ControllerWrapper = (props: ControllerWrapperProps<T>) => {
|
||||||
const { name, control, ...rest } = props;
|
const { name, control, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={name}
|
|
||||||
control={control}
|
control={control}
|
||||||
|
name={name}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||||
const hasSwitchProps = "isChecked" in rest || "onCheckedChange" in rest;
|
const hasSwitchProps = "isChecked" in rest || "onCheckedChange" in rest;
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { Text } from "@/ui/components/typography";
|
|||||||
|
|
||||||
const ActionContainer = styled(XStack, {
|
const ActionContainer = styled(XStack, {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
minWidth: "$5",
|
|
||||||
gap: "$1",
|
gap: "$1",
|
||||||
|
minWidth: "$5",
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ScreenHeadingProps {
|
interface ScreenHeadingProps {
|
||||||
@@ -19,7 +19,13 @@ interface ScreenHeadingProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ScreenHeading = (props: ScreenHeadingProps) => {
|
export const ScreenHeading = (props: ScreenHeadingProps) => {
|
||||||
const { leadingAction, title, trailingActions, paddingHorizontal = "$4", marginBottom = "$2" } = props;
|
const {
|
||||||
|
leadingAction,
|
||||||
|
title,
|
||||||
|
trailingActions,
|
||||||
|
paddingHorizontal = "$4",
|
||||||
|
marginBottom = "$2",
|
||||||
|
} = props;
|
||||||
const trailingActionsArray = Array.isArray(trailingActions)
|
const trailingActionsArray = Array.isArray(trailingActions)
|
||||||
? trailingActions
|
? trailingActions
|
||||||
: trailingActions
|
: trailingActions
|
||||||
@@ -29,16 +35,16 @@ export const ScreenHeading = (props: ScreenHeadingProps) => {
|
|||||||
return (
|
return (
|
||||||
<XStack
|
<XStack
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
|
||||||
height="$6"
|
|
||||||
backgroundColor="$background"
|
backgroundColor="$background"
|
||||||
paddingHorizontal={paddingHorizontal}
|
height="$6"
|
||||||
|
justifyContent="space-between"
|
||||||
marginBottom={marginBottom}
|
marginBottom={marginBottom}
|
||||||
|
paddingHorizontal={paddingHorizontal}
|
||||||
>
|
>
|
||||||
<ActionContainer>{leadingAction}</ActionContainer>
|
<ActionContainer>{leadingAction}</ActionContainer>
|
||||||
<XStack flex={1} justifyContent="center">
|
<XStack flex={1} justifyContent="center">
|
||||||
{title ? (
|
{title ? (
|
||||||
<Text fontWeight="600" fontSize="$6">
|
<Text fontSize="$6" fontWeight="600">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { ArrowRight } from "@tamagui/lucide-icons";
|
import { ArrowRight } from "@tamagui/lucide-icons";
|
||||||
import { Href, Link } from "expo-router";
|
import { Href, Link } from "expo-router";
|
||||||
import { GetProps, Paragraph, styled, XStack } from "tamagui";
|
import { GetProps, Paragraph, styled, XStack } from "tamagui";
|
||||||
@@ -9,8 +7,8 @@ import { Text } from "@/ui/components/typography";
|
|||||||
const SectionContainer = styled(XStack, {
|
const SectionContainer = styled(XStack, {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
width: "100%",
|
|
||||||
paddingVertical: "$2",
|
paddingVertical: "$2",
|
||||||
|
width: "100%",
|
||||||
});
|
});
|
||||||
|
|
||||||
type ScreenSectionProps = GetProps<typeof SectionContainer> & {
|
type ScreenSectionProps = GetProps<typeof SectionContainer> & {
|
||||||
@@ -23,8 +21,8 @@ type ScreenSectionLinkProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ScreenSectionLink = ({ href }: ScreenSectionLinkProps) => (
|
const ScreenSectionLink = ({ href }: ScreenSectionLinkProps) => (
|
||||||
<Link href={href} push asChild>
|
<Link asChild href={href} push>
|
||||||
<XStack gap="2" alignItems="center">
|
<XStack alignItems="center" gap="2">
|
||||||
<Paragraph color="$accent5" fontWeight={500}>
|
<Paragraph color="$accent5" fontWeight={500}>
|
||||||
Voir tout
|
Voir tout
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
@@ -38,7 +36,14 @@ export const ScreenSection = (props: ScreenSectionProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer {...rest}>
|
<SectionContainer {...rest}>
|
||||||
<Text fontSize="$6" fontWeight="bold" color="$color" numberOfLines={1} flexShrink={1} marginRight="$2">
|
<Text
|
||||||
|
color="$color"
|
||||||
|
flexShrink={1}
|
||||||
|
fontSize="$6"
|
||||||
|
fontWeight="bold"
|
||||||
|
marginRight="$2"
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{forwardLink && <ScreenSectionLink href={forwardLink} />}
|
{forwardLink && <ScreenSectionLink href={forwardLink} />}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
|
import React from "react";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { styled, YStack } from "tamagui";
|
import { styled, YStack } from "tamagui";
|
||||||
|
|
||||||
@@ -20,9 +19,9 @@ type ScreenViewComponent = React.FC<React.PropsWithChildren<ScreenViewProps>> &
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ScreenContent = styled(YStack, {
|
const ScreenContent = styled(YStack, {
|
||||||
|
alignItems: "center",
|
||||||
gap: "$4",
|
gap: "$4",
|
||||||
paddingHorizontal: "$4",
|
paddingHorizontal: "$4",
|
||||||
alignItems: "center",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ScreenView: ScreenViewComponent = (props: React.PropsWithChildren<ScreenViewProps>) => {
|
const ScreenView: ScreenViewComponent = (props: React.PropsWithChildren<ScreenViewProps>) => {
|
||||||
@@ -40,7 +39,7 @@ const ScreenView: ScreenViewComponent = (props: React.PropsWithChildren<ScreenVi
|
|||||||
const otherChildren: React.ReactNode[] = [];
|
const otherChildren: React.ReactNode[] = [];
|
||||||
|
|
||||||
// Iterate through children to find the Heading and separate others
|
// Iterate through children to find the Heading and separate others
|
||||||
React.Children.forEach(children, child => {
|
React.Children.forEach(children, (child) => {
|
||||||
if (React.isValidElement(child)) {
|
if (React.isValidElement(child)) {
|
||||||
if (child.type === ScreenView.Heading) {
|
if (child.type === ScreenView.Heading) {
|
||||||
headingElement = child;
|
headingElement = child;
|
||||||
@@ -54,9 +53,11 @@ const ScreenView: ScreenViewComponent = (props: React.PropsWithChildren<ScreenVi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showStatusBar ? <StatusBar style={statusBarStyle} backgroundColor={statusBarBackgroundColor} /> : null}
|
{showStatusBar ? (
|
||||||
|
<StatusBar backgroundColor={statusBarBackgroundColor} style={statusBarStyle} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
<YStack flex={1} paddingTop={insets.top} backgroundColor="$background">
|
<YStack backgroundColor="$background" flex={1} paddingTop={insets.top}>
|
||||||
{headingElement}
|
{headingElement}
|
||||||
|
|
||||||
<ScreenContent
|
<ScreenContent
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const Caption = (props: React.PropsWithChildren<ParagraphProps>) => {
|
|||||||
const { children, ...rest } = props;
|
const { children, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paragraph fontSize="$2" lineHeight="$1" color="$gray10" {...rest}>
|
<Paragraph color="$gray10" fontSize="$2" lineHeight="$1" {...rest}>
|
||||||
{children}
|
{children}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const Heading = (props: React.PropsWithChildren<ParagraphProps>) => {
|
|||||||
const { children, ...rest } = props;
|
const { children, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<H4 fontWeight="bold" alignSelf="flex-start" {...rest}>
|
<H4 alignSelf="flex-start" fontWeight="bold" {...rest}>
|
||||||
{children}
|
{children}
|
||||||
</H4>
|
</H4>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
export { Caption } from "@/ui/components/typography/Caption";
|
||||||
export { Display } from "@/ui/components/typography/Display";
|
export { Display } from "@/ui/components/typography/Display";
|
||||||
export { Heading } from "@/ui/components/typography/Heading";
|
export { Heading } from "@/ui/components/typography/Heading";
|
||||||
export { Caption } from "@/ui/components/typography/Caption";
|
|
||||||
export { Text } from "@/ui/components/typography/Text";
|
export { Text } from "@/ui/components/typography/Text";
|
||||||
|
|||||||
@@ -52,22 +52,14 @@ const darkShadows = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const builtThemes = createThemes({
|
const builtThemes = createThemes({
|
||||||
componentThemes: defaultComponentThemes,
|
accent: {
|
||||||
base: {
|
|
||||||
palette: {
|
palette: {
|
||||||
dark: darkPalette,
|
dark: [...primaryPalette].reverse(),
|
||||||
light: lightPalette,
|
light: [...primaryPalette].reverse(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
base: {
|
||||||
extra: {
|
extra: {
|
||||||
light: {
|
|
||||||
...Colors.green,
|
|
||||||
...Colors.red,
|
|
||||||
...Colors.yellow,
|
|
||||||
...Colors.gray,
|
|
||||||
...lightShadows,
|
|
||||||
shadowColor: lightShadows.shadow1,
|
|
||||||
},
|
|
||||||
dark: {
|
dark: {
|
||||||
...Colors.greenDark,
|
...Colors.greenDark,
|
||||||
...Colors.redDark,
|
...Colors.redDark,
|
||||||
@@ -76,19 +68,25 @@ const builtThemes = createThemes({
|
|||||||
...darkShadows,
|
...darkShadows,
|
||||||
shadowColor: darkShadows.shadow1,
|
shadowColor: darkShadows.shadow1,
|
||||||
},
|
},
|
||||||
|
light: {
|
||||||
|
...Colors.green,
|
||||||
|
...Colors.red,
|
||||||
|
...Colors.yellow,
|
||||||
|
...Colors.gray,
|
||||||
|
...lightShadows,
|
||||||
|
shadowColor: lightShadows.shadow1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
accent: {
|
|
||||||
palette: {
|
palette: {
|
||||||
dark: [...primaryPalette].reverse(),
|
dark: darkPalette,
|
||||||
light: [...primaryPalette].reverse(),
|
light: lightPalette,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
childrenThemes: {
|
childrenThemes: {
|
||||||
primary: {
|
error: {
|
||||||
palette: {
|
palette: {
|
||||||
dark: [...primaryPalette].reverse(),
|
dark: Object.values(Colors.redDark),
|
||||||
light: [...primaryPalette].reverse(),
|
light: Object.values(Colors.red),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gray: {
|
gray: {
|
||||||
@@ -97,16 +95,10 @@ const builtThemes = createThemes({
|
|||||||
light: lightPalette,
|
light: lightPalette,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
warning: {
|
primary: {
|
||||||
palette: {
|
palette: {
|
||||||
dark: Object.values(Colors.yellowDark),
|
dark: [...primaryPalette].reverse(),
|
||||||
light: Object.values(Colors.yellow),
|
light: [...primaryPalette].reverse(),
|
||||||
},
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
palette: {
|
|
||||||
dark: Object.values(Colors.redDark),
|
|
||||||
light: Object.values(Colors.red),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
@@ -115,7 +107,14 @@ const builtThemes = createThemes({
|
|||||||
light: Object.values(Colors.green),
|
light: Object.values(Colors.green),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
warning: {
|
||||||
|
palette: {
|
||||||
|
dark: Object.values(Colors.yellowDark),
|
||||||
|
light: Object.values(Colors.yellow),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
componentThemes: defaultComponentThemes,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Themes = typeof builtThemes;
|
export type Themes = typeof builtThemes;
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "expo/tsconfig.base",
|
|
||||||
"exclude": ["__tests__/**/*-test.ts"],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"],
|
||||||
"./src/*"
|
"~/*": ["./*"]
|
||||||
],
|
|
||||||
"~/*": [
|
|
||||||
"./*"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": [
|
"strict": true
|
||||||
"**/*.ts",
|
},
|
||||||
"**/*.tsx",
|
"exclude": ["__tests__/**/*-test.ts"],
|
||||||
".expo/types/**/*.ts",
|
"extends": "expo/tsconfig.base",
|
||||||
"expo-env.d.ts"
|
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-22
@@ -1,48 +1,48 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "mobile",
|
|
||||||
"slug": "mobile",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"icon": "./assets/images/icon.png",
|
|
||||||
"scheme": "mobile",
|
|
||||||
"userInterfaceStyle": "automatic",
|
|
||||||
"newArchEnabled": true,
|
|
||||||
"ios": {
|
|
||||||
"supportsTablet": true
|
|
||||||
},
|
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
|
||||||
"backgroundImage": "./assets/images/android-icon-background.png",
|
"backgroundImage": "./assets/images/android-icon-background.png",
|
||||||
|
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
||||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false
|
||||||
},
|
},
|
||||||
"web": {
|
"experiments": {
|
||||||
"output": "static",
|
"reactCompiler": true,
|
||||||
"favicon": "./assets/images/favicon.png"
|
"typedRoutes": true
|
||||||
},
|
},
|
||||||
|
"icon": "./assets/images/icon.png",
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"name": "mobile",
|
||||||
|
"newArchEnabled": true,
|
||||||
|
"orientation": "portrait",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
"image": "./assets/images/splash-icon.png",
|
|
||||||
"imageWidth": 200,
|
|
||||||
"resizeMode": "contain",
|
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"dark": {
|
"dark": {
|
||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
}
|
},
|
||||||
|
"image": "./assets/images/splash-icon.png",
|
||||||
|
"imageWidth": 200,
|
||||||
|
"resizeMode": "contain"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"scheme": "mobile",
|
||||||
"typedRoutes": true,
|
"slug": "mobile",
|
||||||
"reactCompiler": true
|
"userInterfaceStyle": "automatic",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/images/favicon.png",
|
||||||
|
"output": "static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export default function Index() {
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>Edit app/index.tsx to edit this screen.</Text>
|
<Text>Edit app/index.tsx to edit this screen.</Text>
|
||||||
|
|||||||
+14
-14
@@ -1,15 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@basango/mobile",
|
|
||||||
"main": "expo-router/entry",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"scripts": {
|
|
||||||
"start": "expo start",
|
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
|
||||||
"android": "expo start --android",
|
|
||||||
"ios": "expo start --ios",
|
|
||||||
"web": "expo start --web",
|
|
||||||
"lint": "expo lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
@@ -31,15 +20,26 @@
|
|||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-worklets": "0.5.1",
|
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0"
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-worklets": "0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"private": true
|
"main": "expo-router/entry",
|
||||||
|
"name": "@basango/mobile",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"lint": "expo lint",
|
||||||
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
|
"start": "expo start",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user