style(biome): using biome for format, lint, check

This commit is contained in:
2025-11-08 12:58:40 +02:00
parent 075a388ccb
commit fdd1cbbfd5
152 changed files with 3737 additions and 3989 deletions
+1 -1
View File
@@ -1 +1 @@
bun run check-types && bun run check && bun run lint:check bun run typecheck && bun run lint
+8 -10
View File
@@ -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
}
} }
+2 -5
View File
@@ -1,9 +1,6 @@
{ {
"version": "1", "ignore": ["node_modules", ".git"],
"name": "basango", "name": "basango",
"type": "collection", "type": "collection",
"ignore": [ "version": "1"
"node_modules",
".git"
]
} }
+59 -65
View File
@@ -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"
}
}
}
} }
+5 -5
View File
@@ -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"
} }
+1 -1
View File
@@ -5,7 +5,7 @@
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"extends": "@basango/typescript-config/base.json", "extends": "@basango/tsconfig/base.json",
"include": ["src"], "include": ["src"],
"references": [] "references": []
} }
+2 -2
View File
@@ -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);
} }
+4 -8
View File
@@ -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
View File
@@ -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>
+13 -13
View File
@@ -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
} }
+2 -2
View File
@@ -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
View File
@@ -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"
} }
} }
+3 -9
View File
@@ -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
View File
@@ -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"
} }
} }
+2 -2
View File
@@ -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 -1
View File
@@ -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"]],
}, },
}; };
+4 -4
View File
@@ -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": {}
} }
-56
View File
@@ -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
} }
} }
@@ -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
} }
} }
@@ -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
} }
} }
+52 -52
View File
@@ -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"
} }
+17 -17
View File
@@ -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)),
); );
} }
+3 -3
View File
@@ -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",
}), }),
}); });
+29 -12
View File
@@ -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&#39;article Consulter l&#39;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&#39;actualités intelligente</Text> <Text>Bienvenue sur Basango, la plateforme d&#39;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&#39;utilisation de Basango et reconnaissez avoir lu En continuant, vous acceptez les conditions d&#39;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&#39;avez pas de compte ? Créer un compte</Text> <Text>Vous n&#39;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&#39;utilisation de Basango et reconnaissez avoir lu En continuant, vous acceptez les conditions d&#39;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&#39;actualités intelligente qui vous aide à rester informé sur La première plateforme d&#39;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&#39;utilisation de Basango et reconnaissez avoir lu En continuant, vous acceptez les conditions d&#39;utilisation de Basango et reconnaissez
notre politique de confidentialité. avoir lu notre politique de confidentialité.
</Caption> </Caption>
</YStack> </YStack>
</ScreenView> </ScreenView>
+5 -5
View File
@@ -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&#39;est produite Une erreur s&#39;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>
+5 -6
View File
@@ -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>
+5 -1
View File
@@ -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 darticles disponibles pour le moment.</Text>} ListEmptyComponent={() => <Text>Pas darticles 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";
+27 -28
View File
@@ -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;
+7 -16
View File
@@ -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
View File
@@ -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"
} }
} }
} }
+1 -1
View File
@@ -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
View File
@@ -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