Merge branch 'develop'
This commit is contained in:
commit
88b3dcb4d1
54 changed files with 6182 additions and 1925 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,7 +7,7 @@ yarn-error.log*
|
|||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
*node_modules
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ export type Json =
|
|||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instanciate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "12.2.3 (519615d)"
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
devis: {
|
||||
|
|
@ -367,21 +372,25 @@ export type Database = {
|
|||
}
|
||||
}
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
||||
|
||||
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
|
|
@ -399,14 +408,16 @@ export type Tables<
|
|||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
|
|
@ -422,14 +433,16 @@ export type TablesInsert<
|
|||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
|
|
@ -445,14 +458,16 @@ export type TablesUpdate<
|
|||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
> = DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
|
|
@ -460,14 +475,16 @@ export type Enums<
|
|||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
> = PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
|
|
|||
11
justfile
11
justfile
|
|
@ -20,4 +20,13 @@ dev:
|
|||
just _api-dev & (just _frontend-dev)
|
||||
|
||||
update-types:
|
||||
npx supabase gen types typescript --project-id "mhcafqvzbrrwvahpvvzd" --schema public > ui/src/types/database.types.ts && cp ui/src/types/database.types.ts api/src/database.types.ts
|
||||
npx supabase gen types typescript --project-id "mhcafqvzbrrwvahpvvzd" --schema public > ui/src/types/database.types.ts && cp ui/src/types/database.types.ts api/src/database.types.ts && cp ui/src/types/database.types.ts xtablo-expo/lib/database.types.ts
|
||||
|
||||
expo-install-all:
|
||||
cd xtablo-expo && npx expo install -- --legacy-peer-deps
|
||||
|
||||
expo-install +package:
|
||||
cd xtablo-expo && npx expo install {{package}} -- --legacy-peer-deps
|
||||
|
||||
expo-start *args:
|
||||
cd xtablo-expo && npx expo start {{args}}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"build:staging": "tsc -b && vite build --mode staging",
|
||||
"build:prod": "tsc -b && vite build --mode production",
|
||||
"deploy:staging": "pnpm run build:staging && wrangler deploy",
|
||||
"deploy:prod": "pnpm run build:prod && wrangler deploy",
|
||||
"deploy:prod": "pnpm run build:prod && wrangler deploy",
|
||||
"cf-typegen": "wrangler types",
|
||||
"test": "vitest run --mode dev",
|
||||
"test:watch": "vitest watch",
|
||||
|
|
@ -61,6 +61,8 @@
|
|||
"wrangler": "^4.24.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@datadog/browser-rum": "^6.13.0",
|
||||
"@datadog/browser-rum-react": "^6.13.0",
|
||||
"@react-stately/calendar": "^3.7.1",
|
||||
"@supabase/supabase-js": "^2.49.3",
|
||||
"@tailwindcss/vite": "^4.0.14",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@datadog/browser-rum':
|
||||
specifier: ^6.13.0
|
||||
version: 6.13.0
|
||||
'@datadog/browser-rum-react':
|
||||
specifier: ^6.13.0
|
||||
version: 6.13.0(@datadog/browser-rum@6.13.0)(react-router-dom@7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
'@react-stately/calendar':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1(react@19.0.0)
|
||||
|
|
@ -430,6 +436,37 @@ packages:
|
|||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@datadog/browser-core@6.13.0':
|
||||
resolution: {integrity: sha512-wWhQ22F8pDjh7kGtvBetJhX5luyKsIVzJQTisjM/6p+A2nchDuoOiZ0GdjajRA5LlPa9pj/PlOoe/g+B9AMELg==}
|
||||
|
||||
'@datadog/browser-rum-core@6.13.0':
|
||||
resolution: {integrity: sha512-+gp9MEDc24xB5dwvVYsOPoRueujTOy0GNW2Q3XNyWMstzuSQIwXaIuDIRVhCoi8+i4UioUCd7ZO30yEB01Ajwg==}
|
||||
|
||||
'@datadog/browser-rum-react@6.13.0':
|
||||
resolution: {integrity: sha512-r53c4soTqiAVK96raOQVaci163lhX6/DVXXGZteHW36q+OMTbIIEaz/jyNX+4JrXORFNrz8b1dWELt49CkDX5w==}
|
||||
peerDependencies:
|
||||
'@datadog/browser-rum': '*'
|
||||
'@datadog/browser-rum-slim': '*'
|
||||
react: 18 || 19
|
||||
react-router-dom: 6 || 7
|
||||
peerDependenciesMeta:
|
||||
'@datadog/browser-rum':
|
||||
optional: true
|
||||
'@datadog/browser-rum-slim':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-router-dom:
|
||||
optional: true
|
||||
|
||||
'@datadog/browser-rum@6.13.0':
|
||||
resolution: {integrity: sha512-VIVpoD+A2WLIskB0PXzO0Gh3bZs7MkOrzhnDpamWMrcbGH/UaX6BUmTlorqtT374Jrre6frzN2IEKGQs7kVamQ==}
|
||||
peerDependencies:
|
||||
'@datadog/browser-logs': 6.13.0
|
||||
peerDependenciesMeta:
|
||||
'@datadog/browser-logs':
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.4.4':
|
||||
resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==}
|
||||
|
||||
|
|
@ -5528,6 +5565,26 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@datadog/browser-core@6.13.0': {}
|
||||
|
||||
'@datadog/browser-rum-core@6.13.0':
|
||||
dependencies:
|
||||
'@datadog/browser-core': 6.13.0
|
||||
|
||||
'@datadog/browser-rum-react@6.13.0(@datadog/browser-rum@6.13.0)(react-router-dom@7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@datadog/browser-core': 6.13.0
|
||||
'@datadog/browser-rum-core': 6.13.0
|
||||
optionalDependencies:
|
||||
'@datadog/browser-rum': 6.13.0
|
||||
react: 19.0.0
|
||||
react-router-dom: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
|
||||
'@datadog/browser-rum@6.13.0':
|
||||
dependencies:
|
||||
'@datadog/browser-core': 6.13.0
|
||||
'@datadog/browser-rum-core': 6.13.0
|
||||
|
||||
'@emnapi/runtime@1.4.4':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
|
|
|||
133
ui/src/App.tsx
133
ui/src/App.tsx
|
|
@ -1,116 +1,49 @@
|
|||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import { LoginPage } from "./pages/login";
|
||||
import { SignUpPage } from "./pages/signup";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
|
||||
import { ThemeProvider } from "@ui/contexts/ThemeContext";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ResetPasswordPage } from "./pages/reset-password";
|
||||
import { LandingPage } from "./pages/landing";
|
||||
import { PublicRoute } from "./components/PublicRoute";
|
||||
import { TabloPage } from "./pages/tablo";
|
||||
import { SessionProvider } from "./contexts/SessionContext";
|
||||
import { OAuthSigninPage } from "./pages/oauth-signin";
|
||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||
import { Layout } from "./components/Layout";
|
||||
import { DevisPage } from "./pages/devis";
|
||||
import { FacturesPage } from "./pages/factures";
|
||||
import { PlanningPage } from "./pages/planning";
|
||||
import { ChantiersPage } from "./pages/chantiers";
|
||||
import { ChatPage } from "./pages/chat";
|
||||
import { FeedbackPage } from "./pages/feedback";
|
||||
import { SupportPage } from "./pages/support";
|
||||
import { SessionProvider } from "@ui/contexts/SessionContext";
|
||||
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
||||
import ChatProvider from "./providers/ChatProvider";
|
||||
import { UserStoreProvider } from "./providers/UserStoreProvider";
|
||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
import { JoinPage } from "@ui/pages/join";
|
||||
import { CreateEventModal } from "./components/CreateEventModal";
|
||||
import { isProd } from "./utils/helpers";
|
||||
import { UserStoreProvider } from "@ui/providers/UserStoreProvider";
|
||||
import { isProd } from "@ui/utils/helpers";
|
||||
import { DatadogRumProvider } from "@ui/providers/DatadogRumProvider";
|
||||
import { routes } from "@ui/lib/routes";
|
||||
|
||||
// Register all Community features
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
const AppRoutes = () => {
|
||||
const element = useRoutes(routes);
|
||||
return element;
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<SessionProvider>
|
||||
<UserStoreProvider>
|
||||
<Router>
|
||||
<div
|
||||
className={twMerge(
|
||||
"min-h-screen",
|
||||
!isProd ? "bg-orange-50" : "bg-white",
|
||||
"dark:bg-gray-900"
|
||||
)}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<TabloPage />} />
|
||||
<Route path="tablo" element={<TabloPage />} />
|
||||
<Route path="devis" element={<DevisPage />} />
|
||||
<Route path="factures" element={<FacturesPage />} />
|
||||
<Route path="planning" element={<PlanningPage />}>
|
||||
<Route index />
|
||||
<Route path=":tablo_id" />
|
||||
<Route path="create" element={<CreateEventModal />} />
|
||||
</Route>
|
||||
<Route path="kanban" element={<NotFoundPage />}>
|
||||
<Route index />
|
||||
<Route path=":tablo_id" />
|
||||
</Route>
|
||||
<Route path="chantiers" element={<ChantiersPage />} />
|
||||
<Route
|
||||
path="chat"
|
||||
element={
|
||||
<ChatProvider>
|
||||
{(client) => <ChatPage client={client} />}
|
||||
</ChatProvider>
|
||||
}
|
||||
>
|
||||
<Route index />
|
||||
<Route path=":channelId" />
|
||||
</Route>
|
||||
<Route path="feedback" element={<FeedbackPage />} />
|
||||
<Route path="support" element={<SupportPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute
|
||||
fallback="/login"
|
||||
shouldRedirectToCurrentPage
|
||||
/>
|
||||
<DatadogRumProvider>
|
||||
<div
|
||||
className={twMerge(
|
||||
"min-h-screen",
|
||||
!isProd ? "bg-orange-50" : "bg-white",
|
||||
"dark:bg-gray-900"
|
||||
)}
|
||||
>
|
||||
<AppRoutes />
|
||||
<style>
|
||||
{`
|
||||
@keyframes slide {
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(100vw); }
|
||||
}
|
||||
>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="join/:tablo_name" element={<JoinPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
|
||||
<Route path="landing" element={<LandingPage />} />
|
||||
<Route element={<PublicRoute />}>
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="signup" element={<SignUpPage />} />
|
||||
<Route
|
||||
path="reset-password"
|
||||
element={<ResetPasswordPage />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
<style>
|
||||
{`
|
||||
@keyframes slide {
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(100vw); }
|
||||
}
|
||||
.animate-slide {
|
||||
animation: slide 24s linear infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
.animate-slide {
|
||||
animation: slide 24s linear infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
</DatadogRumProvider>
|
||||
</Router>
|
||||
</UserStoreProvider>
|
||||
</SessionProvider>
|
||||
|
|
|
|||
139
ui/src/lib/routes.tsx
Normal file
139
ui/src/lib/routes.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { RouteObject } from "react-router-dom";
|
||||
import { ProtectedRoute } from "@ui/components/ProtectedRoute";
|
||||
import { Layout } from "@ui/components/Layout";
|
||||
import { TabloPage } from "@ui/pages/tablo";
|
||||
import { DevisPage } from "@ui/pages/devis";
|
||||
import { FacturesPage } from "@ui/pages/factures";
|
||||
import { PlanningPage } from "@ui/pages/planning";
|
||||
import { NotFoundPage } from "@ui/pages/NotFoundPage";
|
||||
import { JoinPage } from "@ui/pages/join";
|
||||
import { OAuthSigninPage } from "@ui/pages/oauth-signin";
|
||||
import { LandingPage } from "@ui/pages/landing";
|
||||
import { LoginPage } from "@ui/pages/login";
|
||||
import { SignUpPage } from "@ui/pages/signup";
|
||||
import { ResetPasswordPage } from "@ui/pages/reset-password";
|
||||
import { PublicRoute } from "@ui/components/PublicRoute";
|
||||
import ChatProvider from "@ui/providers/ChatProvider";
|
||||
import { CreateEventModal } from "@ui/components/CreateEventModal";
|
||||
import { ChantiersPage } from "@ui/pages/chantiers";
|
||||
import { ChatPage } from "@ui/pages/chat";
|
||||
import { FeedbackPage } from "@ui/pages/feedback";
|
||||
import { SupportPage } from "@ui/pages/support";
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
// Protected routes
|
||||
{
|
||||
path: "/",
|
||||
element: <ProtectedRoute fallback="/login" />,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <TabloPage />,
|
||||
},
|
||||
{
|
||||
path: "tablo",
|
||||
element: <TabloPage />,
|
||||
},
|
||||
{
|
||||
path: "devis",
|
||||
element: <DevisPage />,
|
||||
},
|
||||
{
|
||||
path: "factures",
|
||||
element: <FacturesPage />,
|
||||
},
|
||||
{
|
||||
path: "planning",
|
||||
element: <PlanningPage />,
|
||||
children: [
|
||||
{ index: true },
|
||||
{ path: ":tablo_id" },
|
||||
{ path: "create", element: <CreateEventModal /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "kanban",
|
||||
element: <NotFoundPage />,
|
||||
children: [{ index: true }, { path: ":tablo_id" }],
|
||||
},
|
||||
{
|
||||
path: "chantiers",
|
||||
element: <ChantiersPage />,
|
||||
},
|
||||
{
|
||||
path: "chat",
|
||||
element: (
|
||||
<ChatProvider>
|
||||
<ChatPage />
|
||||
</ChatProvider>
|
||||
),
|
||||
children: [{ index: true }, { path: ":channelId" }],
|
||||
},
|
||||
{
|
||||
path: "feedback",
|
||||
element: <FeedbackPage />,
|
||||
},
|
||||
{
|
||||
path: "support",
|
||||
element: <SupportPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Protected routes with redirect to current page
|
||||
{
|
||||
path: "/join/:tablo_name",
|
||||
element: <ProtectedRoute fallback="/login" shouldRedirectToCurrentPage />,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <JoinPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// OAuth signin route
|
||||
{
|
||||
path: "/login-with-oauth",
|
||||
element: <OAuthSigninPage />,
|
||||
},
|
||||
// Landing page
|
||||
{
|
||||
path: "/landing",
|
||||
element: <LandingPage />,
|
||||
},
|
||||
// Public routes (authentication pages)
|
||||
{
|
||||
path: "/",
|
||||
element: <PublicRoute />,
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "signup",
|
||||
element: <SignUpPage />,
|
||||
},
|
||||
{
|
||||
path: "reset-password",
|
||||
element: <ResetPasswordPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Catch-all route for 404
|
||||
{
|
||||
path: "*",
|
||||
element: <NotFoundPage />,
|
||||
},
|
||||
];
|
||||
21
ui/src/lib/rum.ts
Normal file
21
ui/src/lib/rum.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { datadogRum } from "@datadog/browser-rum";
|
||||
import { reactPlugin } from "@datadog/browser-rum-react";
|
||||
|
||||
export const initRum = () => {
|
||||
datadogRum.init({
|
||||
applicationId: "8e268e1a-1be0-44c6-b12a-978530d497c7",
|
||||
clientToken: "pub1761af09ab04e215cc90d34da6ac576b",
|
||||
site: "datadoghq.com",
|
||||
service: "xtablo-ui",
|
||||
env: import.meta.env.MODE,
|
||||
|
||||
// Specify a version number to identify the deployed version of your application in Datadog
|
||||
// version: '1.0.0',
|
||||
sessionSampleRate: 100,
|
||||
sessionReplaySampleRate: 80,
|
||||
defaultPrivacyLevel: "mask-user-input",
|
||||
plugins: [reactPlugin({ router: true })],
|
||||
trackViewsManually: true,
|
||||
startSessionReplayRecordingManually: false,
|
||||
});
|
||||
};
|
||||
|
|
@ -8,20 +8,18 @@ import {
|
|||
} from "stream-chat-react";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { ChannelPreview } from "@ui/components/ChannelPreview";
|
||||
import { StreamChat } from "stream-chat";
|
||||
import { useChannelFromUrl } from "@ui/hooks/channel";
|
||||
import { useTablosList } from "@ui/hooks/tablos";
|
||||
import { CustomChannelHeader } from "@ui/components/CustomChannelHeader";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ChatPage({ client }: { client: StreamChat }) {
|
||||
export function ChatPage() {
|
||||
const user = useUser();
|
||||
const filters = { members: { $in: [user.id] }, type: "messaging" };
|
||||
|
||||
const { client, channel, setActiveChannel } = useChatContext();
|
||||
const { channel: channelFromUrl, isChannelInUrl } = useChannelFromUrl(client);
|
||||
|
||||
const { channel, setActiveChannel } = useChatContext();
|
||||
|
||||
const { data: tablos } = useTablosList();
|
||||
const [isChannelListExpanded, setIsChannelListExpanded] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,28 @@ export function LoginPage() {
|
|||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/landing"
|
||||
className="inline-flex items-center text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
|
||||
Se connecter
|
||||
</h1>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,28 @@ export function SignUpPage() {
|
|||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/landing"
|
||||
className="inline-flex items-center text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
|
||||
Créer un compte
|
||||
</h1>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Chat, useCreateChatClient } from "stream-chat-react";
|
||||
import { useUser } from "./UserStoreProvider";
|
||||
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
|
||||
import { StreamChat } from "stream-chat";
|
||||
|
||||
export default function ChatProvider({
|
||||
children,
|
||||
}: {
|
||||
children: (client: StreamChat) => React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const apiKey = import.meta.env.VITE_STREAM_CHAT_API_KEY as string;
|
||||
const user = useUser();
|
||||
|
|
@ -42,7 +41,7 @@ export default function ChatProvider({
|
|||
|
||||
return (
|
||||
<Chat client={client} theme="team light">
|
||||
{children(client)}
|
||||
{children}
|
||||
</Chat>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
47
ui/src/providers/DatadogRumProvider.tsx
Normal file
47
ui/src/providers/DatadogRumProvider.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { datadogRum } from "@datadog/browser-rum";
|
||||
import { routes } from "@ui/lib/routes";
|
||||
import { initRum } from "@ui/lib/rum";
|
||||
import { useEffect } from "react";
|
||||
import { matchRoutes, RouteMatch, useLocation } from "react-router-dom";
|
||||
|
||||
function computeViewName(routeMatches: RouteMatch[]) {
|
||||
let viewName = "";
|
||||
for (let index = 0; index < routeMatches.length; index++) {
|
||||
const routeMatch = routeMatches[index];
|
||||
const path = routeMatch.route.path;
|
||||
// Skip pathless routes
|
||||
if (!path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
// Handle absolute child route paths
|
||||
viewName = path;
|
||||
} else {
|
||||
// Handle route paths ending with "/"
|
||||
viewName += viewName.endsWith("/") ? path : `/${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
return viewName || "/";
|
||||
}
|
||||
|
||||
export const DatadogRumProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
initRum();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const routeMatches = matchRoutes(routes, location.pathname);
|
||||
const viewName = routeMatches && computeViewName(routeMatches);
|
||||
if (viewName) {
|
||||
datadogRum.startView({ name: viewName });
|
||||
}
|
||||
}, [location.pathname]);
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
|
@ -7,6 +7,11 @@ export type Json =
|
|||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instanciate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "12.2.3 (519615d)"
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
devis: {
|
||||
|
|
@ -367,21 +372,25 @@ export type Database = {
|
|||
}
|
||||
}
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
||||
|
||||
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
|
|
@ -399,14 +408,16 @@ export type Tables<
|
|||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
|
|
@ -422,14 +433,16 @@ export type TablesInsert<
|
|||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
|
|
@ -445,14 +458,16 @@ export type TablesUpdate<
|
|||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
> = DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
|
|
@ -460,14 +475,16 @@ export type Enums<
|
|||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
> = PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
EXPO_PUBLIC_STREAM_CHAT_API_KEY="t5vvvddteapa"
|
||||
|
||||
EXPO_PUBLIC_SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM
|
||||
|
||||
EXPO_PUBLIC_API_URL=http://192.168.1.110:8080
|
||||
2
xtablo-expo/.gitignore
vendored
2
xtablo-expo/.gitignore
vendored
|
|
@ -36,3 +36,5 @@ yarn-error.*
|
|||
*.tsbuildinfo
|
||||
|
||||
app-example
|
||||
|
||||
ios
|
||||
|
|
@ -1,21 +1,26 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "xtablo-expo",
|
||||
"slug": "xtablo-expo",
|
||||
"name": "xtablo",
|
||||
"slug": "xtablo",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "myapp",
|
||||
"scheme": "com.xtablo.app",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.xtablo.app",
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"package": "com.xtablo.app"
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
|
|
@ -32,10 +37,17 @@
|
|||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-secure-store"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
},
|
||||
"extra": {
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "a3acc825-7c18-4cd4-83c7-60836639decc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
import { Redirect, Slot, Stack } from "expo-router";
|
||||
import { Redirect, Slot } from "expo-router";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { ActivityIndicator, AppState } from "react-native";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
// Tells Supabase Auth to continuously refresh the session automatically if
|
||||
// the app is in the foreground. When this is added, you will continue to receive
|
||||
// `onAuthStateChange` events with the `TOKEN_REFRESHED` or `SIGNED_OUT` event
|
||||
// if the user's session is terminated. This should only be registered once.
|
||||
AppState.addEventListener("change", (state) => {
|
||||
if (state === "active") {
|
||||
supabase.auth.startAutoRefresh();
|
||||
} else {
|
||||
supabase.auth.stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { useGetUser } from "@/hooks/user";
|
||||
import { useEffect } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export default function AuthLayout() {
|
||||
const { session, loading } = useAuth();
|
||||
if (loading) {
|
||||
const { loading, initialize } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { user, isLoading: isUserLoading } = useGetUser();
|
||||
|
||||
const isLoading = loading || isUserLoading;
|
||||
|
||||
useEffect(() => {
|
||||
initialize(queryClient);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
if (session) {
|
||||
if (user) {
|
||||
return <Redirect href="/(home)/(tabs)" />;
|
||||
}
|
||||
return <Slot />;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text } from "react-native";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { Button, Input } from "@rn-vui/themed";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Link } from "expo-router";
|
||||
import { Mail, Lock } from "lucide-react-native";
|
||||
import { GoogleLoginButton } from "@/components/GoogleLoginButton";
|
||||
import { AppleLoginButton } from "@/components/AppleLoginButton";
|
||||
|
||||
export default function Auth() {
|
||||
const [email, setEmail] = useState("");
|
||||
|
|
@ -10,45 +13,58 @@ export default function Auth() {
|
|||
|
||||
const login = useAuth((state) => state.login);
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
const performOAuth = useAuth((state) => state.performOAuth);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Welcome Back!</Text>
|
||||
<Text style={styles.subtitle}>Sign in to your account</Text>
|
||||
<View style={[styles.verticallySpaced, styles.mt40]}>
|
||||
<Image source={require("@/assets/images/logo.png")} style={styles.logo} />
|
||||
<Text style={styles.title}>Connexion XTablo</Text>
|
||||
<Text style={styles.subtitle}>Connectez-vous à votre compte</Text>
|
||||
<View style={[styles.verticallySpaced, styles.mt10]}>
|
||||
<Input
|
||||
label="Email"
|
||||
leftIcon={{ type: "font-awesome", name: "envelope" }}
|
||||
label="Adresse email"
|
||||
leftIcon={<Mail size={20} color="#666" />}
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder="email@address.com"
|
||||
placeholder="jean@dupont.com"
|
||||
autoCapitalize={"none"}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Password"
|
||||
leftIcon={{ type: "font-awesome", name: "lock" }}
|
||||
label="Mot de passe"
|
||||
leftIcon={<Lock size={20} color="#666" />}
|
||||
onChangeText={(text) => setPassword(text)}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
placeholder="Password"
|
||||
placeholder="Mot de passe"
|
||||
autoCapitalize={"none"}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button
|
||||
title="Sign in"
|
||||
title="Se connecter"
|
||||
disabled={authLoading}
|
||||
onPress={() => login(email, password)}
|
||||
buttonStyle={styles.button}
|
||||
titleStyle={styles.buttonTitle}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.separatorContainer}>
|
||||
<View style={styles.separator} />
|
||||
<Text style={styles.separatorText}>ou</Text>
|
||||
<View style={styles.separator} />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<GoogleLoginButton onPress={() => performOAuth("google")} />
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt5]}>
|
||||
<AppleLoginButton onPress={() => performOAuth("apple")} />
|
||||
</View>
|
||||
<View style={styles.linkContainer}>
|
||||
<Text style={styles.linkText}>Don't have an account? </Text>
|
||||
<Text style={styles.linkText}>Pas encore de compte ? </Text>
|
||||
<Link href="/signup" style={styles.link}>
|
||||
Sign Up
|
||||
S'inscrire
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -59,33 +75,46 @@ const styles = StyleSheet.create({
|
|||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
padding: 24,
|
||||
padding: 16,
|
||||
backgroundColor: "#f5f5f5", // Light grey background
|
||||
},
|
||||
logo: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
alignSelf: "center",
|
||||
marginBottom: 8,
|
||||
borderRadius: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 10,
|
||||
marginBottom: 3,
|
||||
color: "#333",
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
marginBottom: 40,
|
||||
marginBottom: 16,
|
||||
color: "#666",
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 8, // Increased padding
|
||||
paddingBottom: 8, // Increased padding
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
marginTop: 12,
|
||||
},
|
||||
mt40: {
|
||||
marginTop: 40, // Increased top margin for the first input
|
||||
},
|
||||
mt10: {
|
||||
marginTop: 6,
|
||||
},
|
||||
mt5: {
|
||||
marginTop: 3,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8, // Rounded corners
|
||||
|
|
@ -95,10 +124,25 @@ const styles = StyleSheet.create({
|
|||
fontWeight: "bold",
|
||||
color: "#fff", // Ensure text is visible on colored button
|
||||
},
|
||||
separatorContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 12,
|
||||
},
|
||||
separator: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: "#ddd",
|
||||
},
|
||||
separatorText: {
|
||||
marginHorizontal: 15,
|
||||
color: "#666",
|
||||
fontSize: 14,
|
||||
},
|
||||
linkContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
marginTop: 20,
|
||||
marginTop: 12,
|
||||
},
|
||||
linkText: {
|
||||
color: "#666",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text } from "react-native";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { Button, Input } from "@rn-vui/themed";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Link } from "expo-router";
|
||||
import { Mail, Lock, User, Building2 } from "lucide-react-native";
|
||||
|
||||
export default function SignUp() {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
|
|
@ -13,42 +17,76 @@ export default function SignUp() {
|
|||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
<Text style={styles.subtitle}>Join us!</Text>
|
||||
<View style={[styles.verticallySpaced, styles.mt40]}>
|
||||
<Image source={require("@/assets/images/logo.png")} style={styles.logo} />
|
||||
<Text style={styles.title}>Créer un compte XTablo</Text>
|
||||
<Text style={styles.subtitle}>Rejoignez-nous !</Text>
|
||||
<View style={[styles.verticallySpaced, styles.mt10]}>
|
||||
<Input
|
||||
label="Email"
|
||||
leftIcon={{ type: "font-awesome", name: "envelope" }}
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder="email@address.com"
|
||||
autoCapitalize={"none"}
|
||||
label="Prénom"
|
||||
leftIcon={<User size={20} color="#666" />}
|
||||
onChangeText={(text) => setFirstName(text)}
|
||||
value={firstName}
|
||||
placeholder="Jean"
|
||||
autoCapitalize={"words"}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Password"
|
||||
leftIcon={{ type: "font-awesome", name: "lock" }}
|
||||
label="Nom"
|
||||
leftIcon={<User size={20} color="#666" />}
|
||||
onChangeText={(text) => setLastName(text)}
|
||||
value={lastName}
|
||||
placeholder="Dupont"
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Nom de l'entreprise"
|
||||
leftIcon={<Building2 size={20} color="#666" />}
|
||||
onChangeText={(text) => setCompanyName(text)}
|
||||
value={companyName}
|
||||
placeholder="Mon Entreprise"
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Adresse email"
|
||||
leftIcon={<Mail size={20} color="#666" />}
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder="jean@dupont.com"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Mot de passe"
|
||||
leftIcon={<Lock size={20} color="#666" />}
|
||||
onChangeText={(text) => setPassword(text)}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
placeholder="Password"
|
||||
autoCapitalize={"none"}
|
||||
secureTextEntry
|
||||
placeholder="Mot de passe"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button
|
||||
title="Sign up"
|
||||
title="S'inscrire"
|
||||
disabled={authLoading}
|
||||
onPress={() => signUp(email, password)}
|
||||
onPress={() =>
|
||||
signUp(email, password, firstName, lastName, companyName)
|
||||
}
|
||||
buttonStyle={styles.button}
|
||||
titleStyle={styles.buttonTitle}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.linkContainer}>
|
||||
<Text style={styles.linkText}>Already have an account? </Text>
|
||||
<Text style={styles.linkText}>Vous avez déjà un compte ? </Text>
|
||||
<Link href="/login" style={styles.link}>
|
||||
Sign In
|
||||
Se connecter
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -60,33 +98,46 @@ const styles = StyleSheet.create({
|
|||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
padding: 24,
|
||||
padding: 16,
|
||||
backgroundColor: "#f5f5f5", // Light grey background
|
||||
},
|
||||
logo: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
alignSelf: "center",
|
||||
marginBottom: 8,
|
||||
borderRadius: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 10,
|
||||
marginBottom: 3,
|
||||
color: "#333",
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
marginBottom: 40,
|
||||
marginBottom: 16,
|
||||
color: "#666",
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
marginTop: 12,
|
||||
},
|
||||
mt40: {
|
||||
marginTop: 40,
|
||||
},
|
||||
mt10: {
|
||||
marginTop: 6,
|
||||
},
|
||||
mt5: {
|
||||
marginTop: 3,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
|
|
@ -96,10 +147,25 @@ const styles = StyleSheet.create({
|
|||
fontWeight: "bold",
|
||||
color: "#fff",
|
||||
},
|
||||
separatorContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 12,
|
||||
},
|
||||
separator: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: "#ddd",
|
||||
},
|
||||
separatorText: {
|
||||
marginHorizontal: 15,
|
||||
color: "#666",
|
||||
fontSize: 14,
|
||||
},
|
||||
linkContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
marginTop: 20,
|
||||
marginTop: 12,
|
||||
},
|
||||
linkText: {
|
||||
color: "#666",
|
||||
|
|
|
|||
|
|
@ -1,41 +1,111 @@
|
|||
import { Tabs } from "expo-router";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import { HapticTab } from "@/components/HapticTab";
|
||||
import { IconSymbol } from "@/components/ui/IconSymbol";
|
||||
import TabBarBackground from "@/components/ui/TabBarBackground";
|
||||
import { Colors } from "@/constants/colors";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
||||
import {
|
||||
MessageCircle,
|
||||
Calendar,
|
||||
List,
|
||||
Home,
|
||||
Grid3X3,
|
||||
} from "lucide-react-native";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
|
||||
headerShown: true,
|
||||
// Colors and theming
|
||||
tabBarActiveTintColor: "#3b82f6", // Modern blue color
|
||||
tabBarInactiveTintColor: isDark ? "#9CA3AF" : "#6B7280",
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
tabBarBackground: TabBarBackground,
|
||||
|
||||
// Enhanced styling
|
||||
tabBarStyle: {
|
||||
backgroundColor: isDark ? "#1F2937" : "#FFFFFF",
|
||||
borderTopWidth: 0,
|
||||
elevation: 20,
|
||||
shadowColor: "#000000",
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 20,
|
||||
height: Platform.OS === "ios" ? 88 : 70,
|
||||
paddingTop: 8,
|
||||
paddingBottom: Platform.OS === "ios" ? 16 : 8,
|
||||
paddingHorizontal: 12,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
// Label styling
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
marginTop: 4,
|
||||
marginBottom: Platform.OS === "ios" ? 0 : 4,
|
||||
},
|
||||
|
||||
// Icon styling
|
||||
tabBarIconStyle: {
|
||||
marginTop: 2,
|
||||
},
|
||||
|
||||
// Animation and interaction
|
||||
tabBarHideOnKeyboard: true,
|
||||
tabBarAllowFontScaling: false,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Home",
|
||||
tabBarIcon: ({ size, color }) => (
|
||||
<IconSymbol size={size} name="house.fill" color={color} />
|
||||
title: "Discussions",
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<MessageCircle
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Discussions",
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
name="planning"
|
||||
options={{
|
||||
title: "Profile",
|
||||
tabBarIcon: ({ size, color }) => (
|
||||
<MaterialCommunityIcons name="account" size={size} color={color} />
|
||||
title: "Planning",
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<Calendar
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Planning",
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="tablos"
|
||||
options={{
|
||||
title: "Tablos",
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<Grid3X3
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Tablos",
|
||||
// Optional: Add a badge for notifications
|
||||
tabBarBadge: undefined, // You can set this to a number for notifications
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,396 @@
|
|||
import { router } from "expo-router";
|
||||
import { ChannelList } from "stream-chat-expo";
|
||||
import { ChannelSort } from "stream-chat";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
} from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Search } from "lucide-react-native";
|
||||
import React from "react";
|
||||
import { useTablosList } from "@/hooks/tablos";
|
||||
import { ColorMap } from "@/constants/colors";
|
||||
import { UserTablo } from "@/types/tablos.types";
|
||||
|
||||
// Custom Avatar Component for Channel List
|
||||
const CustomChannelAvatar = ({
|
||||
channel,
|
||||
tablos,
|
||||
}: {
|
||||
channel: any;
|
||||
tablos: UserTablo[];
|
||||
}) => {
|
||||
const tabloId = channel?.id || "";
|
||||
const tablo = tablos?.find((t) => t.id === tabloId);
|
||||
const tabloColor = tablo?.color || "bg-blue-500";
|
||||
const tabloName = tablo?.name || channel?.data?.name || "Tablo";
|
||||
|
||||
// Get members info
|
||||
const members = channel?.state?.members || {};
|
||||
const memberCount = Object.keys(members).length;
|
||||
|
||||
// Generate initials from tablo name
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
// // Create gradient colors based on tablo color
|
||||
const getTabloGradientColors = (colorKey: string): [string, string] => {
|
||||
const baseColor = ColorMap[colorKey] || ColorMap["bg-blue-500"];
|
||||
|
||||
// Create a lighter version for gradient effect
|
||||
const lightenColor = (hex: string, percent: number): string => {
|
||||
const num = parseInt(hex.replace("#", ""), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = Math.min(255, Math.max(0, (num >> 16) + amt));
|
||||
const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
|
||||
const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt));
|
||||
return "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1);
|
||||
};
|
||||
|
||||
// Create a darker version for gradient effect
|
||||
const darkenColor = (hex: string, percent: number): string => {
|
||||
const num = parseInt(hex.replace("#", ""), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = Math.min(255, Math.max(0, (num >> 16) - amt));
|
||||
const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) - amt));
|
||||
const B = Math.min(255, Math.max(0, (num & 0x0000ff) - amt));
|
||||
return "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1);
|
||||
};
|
||||
|
||||
const lightColor = lightenColor(baseColor, 15);
|
||||
const darkColor = darkenColor(baseColor, 10);
|
||||
|
||||
return [lightColor, darkColor];
|
||||
};
|
||||
|
||||
const initials = getInitials(tabloName);
|
||||
const gradientColors = getTabloGradientColors(tabloColor);
|
||||
|
||||
return (
|
||||
<View style={styles.avatarContainer}>
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.avatarGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Text style={styles.avatarInitials}>{initials}</Text>
|
||||
|
||||
{/* Member count indicator for group channels */}
|
||||
{memberCount > 2 && (
|
||||
<View style={styles.memberCountBadge}>
|
||||
<Text style={styles.memberCountText}>{memberCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Decorative ring */}
|
||||
<View
|
||||
style={[
|
||||
styles.avatarRing,
|
||||
{ borderColor: `${ColorMap[tabloColor]}30` },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Status indicator (online/active) */}
|
||||
<View style={styles.statusIndicator} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function HomeScreen() {
|
||||
const user = useUser();
|
||||
const { data: tablos } = useTablosList();
|
||||
|
||||
const filters = {
|
||||
members: { $in: [user.id] },
|
||||
type: "messaging",
|
||||
};
|
||||
const sort: ChannelSort = { last_updated: -1 };
|
||||
const options = {
|
||||
state: true,
|
||||
watch: true,
|
||||
};
|
||||
|
||||
// Create a wrapper component for the avatar that has access to tablos data
|
||||
const AvatarWithTablos = ({ channel }: { channel: any }) => (
|
||||
<CustomChannelAvatar channel={channel} tablos={tablos || []} />
|
||||
);
|
||||
|
||||
return (
|
||||
<ChannelList
|
||||
onSelect={(channel) => {
|
||||
router.push(`/channel/${channel.cid}`);
|
||||
}}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
||||
{/* Beautiful Header */}
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerBottom}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.headerTitle}>Discussions</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Gérez les conversations de vos tablos
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.searchButton}>
|
||||
<Search size={20} color="#3b82f6" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Decorative Elements
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} /> */}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Channel List */}
|
||||
<View style={styles.channelListContainer}>
|
||||
<ChannelList
|
||||
filters={filters}
|
||||
onSelect={(channel) => {
|
||||
router.push(`/channel/${channel.cid}`);
|
||||
}}
|
||||
sort={sort}
|
||||
options={options}
|
||||
PreviewAvatar={AvatarWithTablos}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
headerGradient: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 25,
|
||||
paddingHorizontal: 20,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
},
|
||||
headerContent: {
|
||||
zIndex: 10,
|
||||
},
|
||||
headerTop: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
userInfo: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
avatar: {
|
||||
marginRight: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
borderWidth: 3,
|
||||
borderColor: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
greetingContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
greeting: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255, 255, 255, 0.9)",
|
||||
fontWeight: "500",
|
||||
},
|
||||
userName: {
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
marginTop: 2,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
},
|
||||
actionButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
},
|
||||
notificationBadge: {
|
||||
position: "absolute",
|
||||
top: -2,
|
||||
right: -2,
|
||||
backgroundColor: "#ef4444",
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 2,
|
||||
borderColor: "white",
|
||||
},
|
||||
badgeText: {
|
||||
color: "white",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
headerBottom: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 28,
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
marginBottom: 4,
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
fontWeight: "400",
|
||||
},
|
||||
searchButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: "white",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: "absolute",
|
||||
top: -50,
|
||||
right: -30,
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: "absolute",
|
||||
bottom: -20,
|
||||
left: -20,
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
channelListContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
marginTop: -10,
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
paddingTop: 10,
|
||||
},
|
||||
|
||||
// Custom Avatar Styles
|
||||
avatarContainer: {
|
||||
position: "relative",
|
||||
width: 56,
|
||||
height: 56,
|
||||
marginRight: 12,
|
||||
},
|
||||
avatarGradient: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
position: "relative",
|
||||
},
|
||||
avatarInitials: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
textShadowColor: "rgba(0, 0, 0, 0.3)",
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
},
|
||||
avatarRing: {
|
||||
position: "absolute",
|
||||
top: -2,
|
||||
left: -2,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 18,
|
||||
borderWidth: 2,
|
||||
borderColor: "rgba(59, 130, 246, 0.2)",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
statusIndicator: {
|
||||
position: "absolute",
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#10b981",
|
||||
borderWidth: 3,
|
||||
borderColor: "white",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
memberCountBadge: {
|
||||
position: "absolute",
|
||||
top: -4,
|
||||
right: -4,
|
||||
backgroundColor: "#3b82f6",
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 2,
|
||||
borderColor: "white",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
memberCountText: {
|
||||
color: "white",
|
||||
fontSize: 11,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
1343
xtablo-expo/app/(home)/(tabs)/planning.tsx
Normal file
1343
xtablo-expo/app/(home)/(tabs)/planning.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,156 +0,0 @@
|
|||
import { View, StyleSheet } from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Avatar, Button, Input } from "@rn-vui/themed";
|
||||
import { Card, ListItem } from "@rn-vui/themed";
|
||||
import { useState } from "react";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const signOut = useAuth((state) => state.signOut);
|
||||
const user = useUser();
|
||||
|
||||
const [displayName, setDisplayName] = useState(user?.full_name || "");
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Card containerStyle={styles.card}>
|
||||
<Avatar
|
||||
size="xlarge"
|
||||
rounded
|
||||
icon={{ name: "user", type: "font-awesome" }}
|
||||
containerStyle={styles.avatar}
|
||||
/>
|
||||
|
||||
<Card.Title style={styles.cardTitle}>{user?.full_name}</Card.Title>
|
||||
<Card.Divider />
|
||||
|
||||
<ListItem key="email" bottomDivider containerStyle={styles.listItem}>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={styles.listItemTitle}>Email</ListItem.Title>
|
||||
<ListItem.Subtitle style={styles.listItemSubtitle}>
|
||||
{user?.email}
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
key="display-name"
|
||||
bottomDivider
|
||||
containerStyle={styles.listItem}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={styles.listItemTitle}>
|
||||
Display Name
|
||||
</ListItem.Title>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
placeholder="Enter display name"
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
containerStyle={styles.inputContainer}
|
||||
inputStyle={styles.inputText}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<ListItem.Subtitle style={styles.listItemSubtitle}>
|
||||
{user?.full_name || "Not Set"}
|
||||
</ListItem.Subtitle>
|
||||
)}
|
||||
</ListItem.Content>
|
||||
<Button
|
||||
icon={{
|
||||
name: isEditing ? "check" : "edit",
|
||||
type: "font-awesome",
|
||||
size: 18,
|
||||
}}
|
||||
type="clear"
|
||||
onPress={() => {
|
||||
if (isEditing) {
|
||||
// handleSaveDisplayName();
|
||||
} else {
|
||||
setDisplayName(user?.full_name || "");
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Button
|
||||
title="Sign Out"
|
||||
onPress={signOut}
|
||||
buttonStyle={styles.signOutButton}
|
||||
titleStyle={styles.signOutText}
|
||||
icon={{
|
||||
name: "sign-out",
|
||||
type: "font-awesome",
|
||||
color: "white",
|
||||
size: 18,
|
||||
}}
|
||||
iconRight
|
||||
/>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
backgroundColor: "#f8f9fa",
|
||||
},
|
||||
card: {
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
margin: 0,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
avatar: {
|
||||
marginBottom: 20,
|
||||
backgroundColor: "#e0e0e0",
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 15,
|
||||
},
|
||||
listItem: {
|
||||
paddingVertical: 15,
|
||||
width: "100%",
|
||||
},
|
||||
listItemTitle: {
|
||||
fontWeight: "600",
|
||||
marginBottom: 5,
|
||||
color: "#555",
|
||||
},
|
||||
listItemSubtitle: {
|
||||
color: "#333",
|
||||
fontSize: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
paddingHorizontal: 0,
|
||||
height: 40,
|
||||
},
|
||||
inputText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
signOutButton: {
|
||||
marginTop: 30,
|
||||
backgroundColor: "#dc3545",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
width: "80%",
|
||||
},
|
||||
signOutText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
1046
xtablo-expo/app/(home)/(tabs)/tablos.tsx
Normal file
1046
xtablo-expo/app/(home)/(tabs)/tablos.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,38 +1,208 @@
|
|||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, SafeAreaView } from "react-native";
|
||||
import { Channel as ChannelType } from "stream-chat";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import {
|
||||
Channel,
|
||||
MessageInput,
|
||||
MessageList,
|
||||
useChatContext,
|
||||
} from "stream-chat-expo";
|
||||
import { MessageCircle, Users, Smile } from "lucide-react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHeaderHeight } from "@react-navigation/elements";
|
||||
|
||||
export default function ChannelScreen() {
|
||||
const [channel, setChannel] = useState<ChannelType | null>(null);
|
||||
const { cid } = useLocalSearchParams<{ cid: string }>();
|
||||
|
||||
const [type, id] = cid.split(":");
|
||||
const { client } = useChatContext();
|
||||
const channel = client.channel(type, id);
|
||||
const [hasMessages, setHasMessages] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const headerHeight = useHeaderHeight();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChannel = async () => {
|
||||
const channels = await client.queryChannels({ cid });
|
||||
setChannel(channels[0]);
|
||||
};
|
||||
fetchChannel();
|
||||
}, [cid]);
|
||||
if (channel) {
|
||||
const checkMessages = async () => {
|
||||
try {
|
||||
// Get channel state to check for messages
|
||||
await channel.watch();
|
||||
const messages = channel.state.messages || [];
|
||||
setHasMessages(messages.length > 0);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkMessages();
|
||||
|
||||
// Listen for new messages
|
||||
const handleNewMessage = () => {
|
||||
setHasMessages(true);
|
||||
};
|
||||
|
||||
channel.on("message.new", handleNewMessage);
|
||||
|
||||
return () => {
|
||||
channel.off("message.new", handleNewMessage);
|
||||
};
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
if (!channel) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
const EmptyState = () => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<MessageCircle size={64} color="#d1d5db" strokeWidth={1.5} />
|
||||
<View style={styles.decorativeElements}>
|
||||
<View style={styles.floatingIcon1}>
|
||||
<Users size={20} color="#e5e7eb" />
|
||||
</View>
|
||||
<View style={styles.floatingIcon2}>
|
||||
<Smile size={18} color="#e5e7eb" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.emptyTitle}>Commencez la conversation</Text>
|
||||
<Text style={styles.emptyMessage}>
|
||||
Soyez le premier à envoyer un message dans ce canal !
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<Channel channel={channel}>
|
||||
<MessageList />
|
||||
<SafeAreaView>
|
||||
<MessageInput />
|
||||
</SafeAreaView>
|
||||
</Channel>
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: "#f8fafc" }}>
|
||||
<Channel channel={channel} keyboardVerticalOffset={headerHeight}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
<Text style={styles.loadingText}>Chargement des messages...</Text>
|
||||
</View>
|
||||
) : hasMessages ? (
|
||||
<MessageList />
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||
style={styles.keyboardContainer}
|
||||
>
|
||||
<MessageInput />
|
||||
</KeyboardAvoidingView>
|
||||
</Channel>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#f8fafc",
|
||||
gap: 16,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 16,
|
||||
color: "#6b7280",
|
||||
fontWeight: "500",
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 40,
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
emptyIconContainer: {
|
||||
position: "relative",
|
||||
marginBottom: 32,
|
||||
width: 120,
|
||||
height: 120,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
decorativeElements: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
floatingIcon1: {
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 5,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 15,
|
||||
padding: 6,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
floatingIcon2: {
|
||||
position: "absolute",
|
||||
bottom: 15,
|
||||
left: 8,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 12,
|
||||
padding: 5,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
marginBottom: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
emptyMessage: {
|
||||
fontSize: 16,
|
||||
color: "#6b7280",
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
marginBottom: 32,
|
||||
},
|
||||
emptyHint: {
|
||||
backgroundColor: "white",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "#e5e7eb",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 1,
|
||||
},
|
||||
emptyHintText: {
|
||||
fontSize: 14,
|
||||
color: "#9ca3af",
|
||||
fontWeight: "500",
|
||||
textAlign: "center",
|
||||
},
|
||||
keyboardContainer: {
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
messageInputContainer: {
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
361
xtablo-expo/app/(home)/user/profile.tsx
Normal file
361
xtablo-expo/app/(home)/user/profile.tsx
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Avatar, Input } from "@rn-vui/themed";
|
||||
import { Card } from "@rn-vui/themed";
|
||||
import { useState } from "react";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import {
|
||||
User,
|
||||
Mail,
|
||||
Edit3,
|
||||
Check,
|
||||
LogOut,
|
||||
Settings,
|
||||
Shield,
|
||||
} from "lucide-react-native";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const signOut = useAuth((state) => state.signOut);
|
||||
const user = useUser();
|
||||
|
||||
const [displayName, setDisplayName] = useState(user.name || "");
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const handleSaveDisplayName = () => {
|
||||
// TODO: Implémenter la fonctionnalité de sauvegarde
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
icon: Settings,
|
||||
title: "Paramètres du compte",
|
||||
subtitle: "Gérez vos préférences de compte",
|
||||
onPress: () => console.log("Paramètres"),
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Confidentialité et sécurité",
|
||||
subtitle: "Contrôlez vos paramètres de confidentialité",
|
||||
onPress: () => console.log("Confidentialité"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<Avatar
|
||||
size={120}
|
||||
rounded
|
||||
source={user.avatar_url ? { uri: user.avatar_url } : undefined}
|
||||
icon={
|
||||
!user.avatar_url
|
||||
? { name: "user", type: "font-awesome" }
|
||||
: undefined
|
||||
}
|
||||
containerStyle={styles.avatar}
|
||||
/>
|
||||
<Text style={styles.userName}>{user.name || "Utilisateur"}</Text>
|
||||
<Text style={styles.userEmail}>{user.email}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Contenu principal */}
|
||||
<View style={styles.content}>
|
||||
{/* Carte d'informations personnelles */}
|
||||
<Card containerStyle={styles.mainCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<User size={20} color="#3b82f6" />
|
||||
<Text style={styles.cardHeaderTitle}>
|
||||
Informations personnelles
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoItem}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<Mail size={18} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={styles.infoLabel}>Adresse e-mail</Text>
|
||||
<Text style={styles.infoValue}>{user.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.infoItem}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<User size={18} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={styles.infoLabel}>Nom d'affichage</Text>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
placeholder="Entrez le nom d'affichage"
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
containerStyle={styles.inputContainer}
|
||||
inputStyle={styles.inputText}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.infoValue}>
|
||||
{user.name || "Non défini"}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.editButton}
|
||||
onPress={() => {
|
||||
if (isEditing) {
|
||||
handleSaveDisplayName();
|
||||
} else {
|
||||
setDisplayName(user.name ?? "");
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<Check size={18} color="#10b981" />
|
||||
) : (
|
||||
<Edit3 size={18} color="#6b7280" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* Éléments de menu */}
|
||||
<Card containerStyle={styles.menuCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Settings size={20} color="#3b82f6" />
|
||||
<Text style={styles.cardHeaderTitle}>Préférences</Text>
|
||||
</View>
|
||||
|
||||
{menuItems.map((item, index) => (
|
||||
<View key={index}>
|
||||
<TouchableOpacity style={styles.menuItem} onPress={item.onPress}>
|
||||
<View style={styles.menuIconContainer}>
|
||||
<item.icon size={20} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.menuContent}>
|
||||
<Text style={styles.menuTitle}>{item.title}</Text>
|
||||
<Text style={styles.menuSubtitle}>{item.subtitle}</Text>
|
||||
</View>
|
||||
<Text style={styles.menuArrow}>›</Text>
|
||||
</TouchableOpacity>
|
||||
{index < menuItems.length - 1 && <View style={styles.divider} />}
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
{/* Bouton de déconnexion */}
|
||||
<TouchableOpacity style={styles.signOutContainer} onPress={signOut}>
|
||||
<LinearGradient
|
||||
colors={["#ef4444", "#dc2626"]}
|
||||
style={styles.signOutGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<LogOut size={20} color="white" />
|
||||
<Text style={styles.signOutText}>Se déconnecter</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
headerGradient: {
|
||||
paddingTop: 60,
|
||||
paddingBottom: 40,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
headerContent: {
|
||||
alignItems: "center",
|
||||
},
|
||||
avatar: {
|
||||
marginBottom: 16,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
borderWidth: 4,
|
||||
borderColor: "white",
|
||||
},
|
||||
userName: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
marginBottom: 4,
|
||||
textAlign: "center",
|
||||
},
|
||||
userEmail: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255, 255, 255, 0.9)",
|
||||
textAlign: "center",
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
marginTop: -20,
|
||||
},
|
||||
mainCard: {
|
||||
borderRadius: 16,
|
||||
padding: 0,
|
||||
marginBottom: 20,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
borderWidth: 0,
|
||||
},
|
||||
menuCard: {
|
||||
borderRadius: 16,
|
||||
padding: 0,
|
||||
marginBottom: 20,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
borderWidth: 0,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
cardHeaderTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
marginLeft: 12,
|
||||
},
|
||||
infoItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
infoIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#f3f4f6",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
infoContent: {
|
||||
flex: 1,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 14,
|
||||
color: "#6b7280",
|
||||
marginBottom: 4,
|
||||
fontWeight: "500",
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 16,
|
||||
color: "#1f2937",
|
||||
fontWeight: "500",
|
||||
},
|
||||
editButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: "#f3f4f6",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
inputContainer: {
|
||||
paddingHorizontal: 0,
|
||||
height: 40,
|
||||
marginTop: 4,
|
||||
},
|
||||
inputText: {
|
||||
fontSize: 16,
|
||||
color: "#1f2937",
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: "#e5e7eb",
|
||||
marginLeft: 72,
|
||||
},
|
||||
menuItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
menuIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#f3f4f6",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
menuContent: {
|
||||
flex: 1,
|
||||
},
|
||||
menuTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
color: "#1f2937",
|
||||
marginBottom: 2,
|
||||
},
|
||||
menuSubtitle: {
|
||||
fontSize: 14,
|
||||
color: "#6b7280",
|
||||
},
|
||||
menuArrow: {
|
||||
fontSize: 20,
|
||||
color: "#9ca3af",
|
||||
fontWeight: "300",
|
||||
},
|
||||
signOutContainer: {
|
||||
marginTop: 10,
|
||||
marginBottom: 40,
|
||||
},
|
||||
signOutGradient: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 12,
|
||||
shadowColor: "#ef4444",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
},
|
||||
signOutText: {
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
|
@ -12,12 +12,25 @@ import "react-native-reanimated";
|
|||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
window.structuredClone = cloneDeep;
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// 3 total attempts (1 initial + 2 retries)
|
||||
retry: 2,
|
||||
// 0s -> 1s, 1s → 5s. Little resiliency 😁
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 5 ** attemptIndex, 10000),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [loaded] = useFonts({
|
||||
|
|
@ -31,7 +44,7 @@ export default function RootLayout() {
|
|||
}, [loaded]);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
BIN
xtablo-expo/assets/images/google.png
Normal file
BIN
xtablo-expo/assets/images/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
BIN
xtablo-expo/assets/images/logo.jpg
Normal file
BIN
xtablo-expo/assets/images/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
xtablo-expo/assets/images/logo.png
Normal file
BIN
xtablo-expo/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
58
xtablo-expo/components/AppleLoginButton.tsx
Normal file
58
xtablo-expo/components/AppleLoginButton.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React from "react";
|
||||
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Svg, Path } from "react-native-svg";
|
||||
|
||||
const AppleIcon = ({ color = "#fff", size = 20 }) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.653-.026 2.681-1.507 3.694-2.961 1.169-1.69 1.648-3.327 1.679-3.418-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.646 1.09z"
|
||||
fill={color}
|
||||
/>
|
||||
<Path
|
||||
d="M15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701z"
|
||||
fill={color}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
export const AppleLoginButton = ({ onPress }: { onPress: () => void }) => {
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={onPress}
|
||||
disabled={authLoading}
|
||||
>
|
||||
<View style={styles.contentWrapper}>
|
||||
<AppleIcon color="#fff" size={20} />
|
||||
<Text style={styles.text}>Continuer avec Apple</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
backgroundColor: "#000000",
|
||||
borderRadius: 8,
|
||||
height: 48,
|
||||
paddingHorizontal: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
contentWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
text: {
|
||||
color: "#ffffff",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
53
xtablo-expo/components/GoogleLoginButton.tsx
Normal file
53
xtablo-expo/components/GoogleLoginButton.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import React from "react";
|
||||
import { StyleSheet, View, Text, TouchableOpacity, Image } from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
|
||||
export const GoogleLoginButton = ({ onPress }: { onPress: () => void }) => {
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={onPress}
|
||||
disabled={authLoading}
|
||||
>
|
||||
<View style={styles.contentWrapper}>
|
||||
<Image
|
||||
source={require("@/assets/images/google.png")}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.text}>Continuer avec Google</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#747775",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
height: 48,
|
||||
paddingHorizontal: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
contentWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
icon: {
|
||||
height: 20,
|
||||
width: 20,
|
||||
marginRight: 12,
|
||||
},
|
||||
text: {
|
||||
color: "#1f1f1f",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +1,70 @@
|
|||
// This is a shim for web and Android where the tab bar is generally opaque.
|
||||
export default undefined;
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
|
||||
export default function TabBarBackground() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<LinearGradient
|
||||
colors={
|
||||
isDark
|
||||
? ["#1F2937", "#111827", "#0F172A"]
|
||||
: ["#FFFFFF", "#F8FAFC", "#F1F5F9"]
|
||||
}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
{/* Subtle top border gradient */}
|
||||
<LinearGradient
|
||||
colors={
|
||||
isDark
|
||||
? ["rgba(59, 130, 246, 0.3)", "transparent"]
|
||||
: ["rgba(59, 130, 246, 0.1)", "transparent"]
|
||||
}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={styles.topBorder}
|
||||
/>
|
||||
|
||||
{/* Optional: Add some decorative elements */}
|
||||
<View
|
||||
style={[
|
||||
styles.decorativeCircle,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(59, 130, 246, 0.05)"
|
||||
: "rgba(59, 130, 246, 0.03)",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBottomTabOverflow() {
|
||||
return 0;
|
||||
return 24; // Account for the rounded corners
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
topBorder: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
},
|
||||
decorativeCircle: {
|
||||
position: "absolute",
|
||||
top: -20,
|
||||
right: 20,
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,24 +3,50 @@
|
|||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
const tintColorLight = '#0a7ea4';
|
||||
const tintColorDark = '#fff';
|
||||
const tintColorLight = "#0a7ea4";
|
||||
const tintColorDark = "#fff";
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: '#11181C',
|
||||
background: '#fff',
|
||||
text: "#11181C",
|
||||
background: "#fff",
|
||||
tint: tintColorLight,
|
||||
icon: '#687076',
|
||||
tabIconDefault: '#687076',
|
||||
icon: "#687076",
|
||||
tabIconDefault: "#687076",
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#ECEDEE',
|
||||
background: '#151718',
|
||||
text: "#ECEDEE",
|
||||
background: "#151718",
|
||||
tint: tintColorDark,
|
||||
icon: '#9BA1A6',
|
||||
tabIconDefault: '#9BA1A6',
|
||||
icon: "#9BA1A6",
|
||||
tabIconDefault: "#9BA1A6",
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
|
||||
export const AVAILABLE_COLORS = [
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-purple-500",
|
||||
"bg-red-500",
|
||||
"bg-yellow-500",
|
||||
"bg-indigo-500",
|
||||
"bg-pink-500",
|
||||
"bg-teal-500",
|
||||
"bg-orange-500",
|
||||
"bg-cyan-500",
|
||||
];
|
||||
|
||||
export const ColorMap: Record<(typeof AVAILABLE_COLORS)[number], string> = {
|
||||
"bg-blue-500": "#3b82f6",
|
||||
"bg-green-500": "#10b981",
|
||||
"bg-red-500": "#ef4444",
|
||||
"bg-yellow-500": "#f59e0b",
|
||||
"bg-purple-500": "#8b5cf6",
|
||||
"bg-pink-500": "#ec4899",
|
||||
"bg-orange-500": "#f97316",
|
||||
"bg-teal-500": "#0d9488",
|
||||
"bg-indigo-500": "#6366f1",
|
||||
"bg-cyan-500": "#06b6d4",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,24 +3,50 @@
|
|||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
const tintColorLight = '#0a7ea4';
|
||||
const tintColorDark = '#fff';
|
||||
const tintColorLight = "#0a7ea4";
|
||||
const tintColorDark = "#fff";
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: '#11181C',
|
||||
background: '#fff',
|
||||
text: "#11181C",
|
||||
background: "#fff",
|
||||
tint: tintColorLight,
|
||||
icon: '#687076',
|
||||
tabIconDefault: '#687076',
|
||||
icon: "#687076",
|
||||
tabIconDefault: "#687076",
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#ECEDEE',
|
||||
background: '#151718',
|
||||
text: "#ECEDEE",
|
||||
background: "#151718",
|
||||
tint: tintColorDark,
|
||||
icon: '#9BA1A6',
|
||||
tabIconDefault: '#9BA1A6',
|
||||
icon: "#9BA1A6",
|
||||
tabIconDefault: "#9BA1A6",
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
|
||||
export const AVAILABLE_COLORS = [
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-purple-500",
|
||||
"bg-red-500",
|
||||
"bg-yellow-500",
|
||||
"bg-indigo-500",
|
||||
"bg-pink-500",
|
||||
"bg-teal-500",
|
||||
"bg-orange-500",
|
||||
"bg-cyan-500",
|
||||
];
|
||||
|
||||
export const ColorMap: Record<(typeof AVAILABLE_COLORS)[number], string> = {
|
||||
"bg-blue-500": "#3b82f6",
|
||||
"bg-green-500": "#10b981",
|
||||
"bg-red-500": "#ef4444",
|
||||
"bg-yellow-500": "#f59e0b",
|
||||
"bg-purple-500": "#8b5cf6",
|
||||
"bg-pink-500": "#ec4899",
|
||||
"bg-orange-500": "#f97316",
|
||||
"bg-teal-500": "#0d9488",
|
||||
"bg-indigo-500": "#6366f1",
|
||||
"bg-cyan-500": "#06b6d4",
|
||||
};
|
||||
|
|
|
|||
21
xtablo-expo/eas.json
Normal file
21
xtablo-expo/eas.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"cli": {
|
||||
"version": ">= 16.1.0",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
59
xtablo-expo/hooks/events.ts
Normal file
59
xtablo-expo/hooks/events.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { EventAndTablo, EventInsert } from "@/types/events.types";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
|
||||
export const useEventsByTablo = (tabloId: string | null) => {
|
||||
return useQuery({
|
||||
queryKey: ["events", tabloId],
|
||||
queryFn: async () => {
|
||||
if (!tabloId) {
|
||||
const { data, error } = await supabase
|
||||
.from("events_and_tablos")
|
||||
.select("*")
|
||||
.order("start_date", { ascending: true })
|
||||
.order("start_time", { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
return data as EventAndTablo[];
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from("events_and_tablos")
|
||||
.select("*")
|
||||
.eq("tablo_id", tabloId)
|
||||
.order("start_date", { ascending: true })
|
||||
.order("start_time", { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
return data as EventAndTablo[];
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateEvent = () => {
|
||||
const user = useUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: async (event: Omit<EventInsert, "created_by">) => {
|
||||
const { data, error } = await supabase
|
||||
.from("events")
|
||||
.insert({
|
||||
...event,
|
||||
created_by: user.id,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data as Event;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["events"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
},
|
||||
});
|
||||
return mutate;
|
||||
};
|
||||
52
xtablo-expo/hooks/tablos.ts
Normal file
52
xtablo-expo/hooks/tablos.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { supabase } from "@/lib/supabase";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { TabloInsert, UserTablo } from "@/types/tablos.types";
|
||||
import { api } from "@/lib/api";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
// type TabloInsert = Tablo["Insert"];
|
||||
// type TabloUpdate = Tablo["Update"];
|
||||
|
||||
// Fetch all tablos
|
||||
export const useTablosList = () => {
|
||||
const user = useUser();
|
||||
return useQuery({
|
||||
queryKey: ["tablos"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("user_tablos")
|
||||
.select("*")
|
||||
.eq("user_id", user.id);
|
||||
if (error) throw error;
|
||||
const tablos = data as UserTablo[];
|
||||
return tablos;
|
||||
},
|
||||
refetchInterval: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateTablo = () => {
|
||||
const session = useAuth((state) => state.session);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
tablo: Pick<TabloInsert, "name" | "color" | "image" | "status">
|
||||
) => {
|
||||
const { data } = await api.post("/api/v1/tablos/create", tablo, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tablos"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
Alert.alert("Erreur", "Impossible de créer le tablo.");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -3,14 +3,14 @@
|
|||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { Colors } from "@/constants/colors";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const theme = useColorScheme() ?? "light";
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,29 @@
|
|||
import { Tables } from "@/lib/database.types";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { Tables } from "@/types/database.types";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type User = Tables<"profiles">;
|
||||
type User = Tables<"profiles"> & {
|
||||
streamToken: string | null;
|
||||
};
|
||||
|
||||
export const useGetUser = (): { user: User | null; isPending: boolean } => {
|
||||
const { data, isPending } = useQuery<User[] | null>({
|
||||
export const useGetUser = (): { user: User | null; isLoading: boolean } => {
|
||||
const session = useAuth((state) => state.session);
|
||||
const { data, isLoading } = useQuery<User | null>({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
const { data } = await supabase.from("profiles").select("*");
|
||||
return data;
|
||||
try {
|
||||
const { data: user } = await api.get<User>("/api/v1/users/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { user: data?.[0] ?? null, isPending };
|
||||
return { user: data ?? null, isLoading };
|
||||
};
|
||||
|
|
|
|||
10
xtablo-expo/lib/api.ts
Normal file
10
xtablo-expo/lib/api.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import axios from "axios";
|
||||
|
||||
// Create axios instance with default config
|
||||
export const api = axios.create({
|
||||
baseURL: process.env.EXPO_PUBLIC_API_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 2000,
|
||||
});
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[];
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
devis: {
|
||||
Row: {
|
||||
client_email: string;
|
||||
created_at: string;
|
||||
date: string;
|
||||
due_date: string;
|
||||
id: string;
|
||||
items: Json;
|
||||
notes: string | null;
|
||||
number: string;
|
||||
status: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
terms: string | null;
|
||||
total: number;
|
||||
updated_at: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
client_email: string;
|
||||
created_at?: string;
|
||||
date: string;
|
||||
due_date: string;
|
||||
id?: string;
|
||||
items?: Json;
|
||||
notes?: string | null;
|
||||
number: string;
|
||||
status?: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
terms?: string | null;
|
||||
total: number;
|
||||
updated_at?: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
client_email?: string;
|
||||
created_at?: string;
|
||||
date?: string;
|
||||
due_date?: string;
|
||||
id?: string;
|
||||
items?: Json;
|
||||
notes?: string | null;
|
||||
number?: string;
|
||||
status?: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal?: number;
|
||||
tax?: number;
|
||||
terms?: string | null;
|
||||
total?: number;
|
||||
updated_at?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
profiles: {
|
||||
Row: {
|
||||
avatar_url: string | null;
|
||||
email: string | null;
|
||||
full_name: string | null;
|
||||
id: string;
|
||||
updated_at: string | null;
|
||||
website: string | null;
|
||||
};
|
||||
Insert: {
|
||||
avatar_url?: string | null;
|
||||
email?: string | null;
|
||||
full_name?: string | null;
|
||||
id: string;
|
||||
updated_at?: string | null;
|
||||
website?: string | null;
|
||||
};
|
||||
Update: {
|
||||
avatar_url?: string | null;
|
||||
email?: string | null;
|
||||
full_name?: string | null;
|
||||
id?: string;
|
||||
updated_at?: string | null;
|
||||
website?: string | null;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
user_tablos: {
|
||||
Row: {
|
||||
id: string;
|
||||
owner_id: string;
|
||||
name: string;
|
||||
image: string | null;
|
||||
color: string | null;
|
||||
status: string;
|
||||
position: number;
|
||||
created_at: string | null;
|
||||
deleted_at: string | null;
|
||||
access_level: "owner" | "admin" | "member";
|
||||
is_admin: boolean;
|
||||
granted_by: string | null;
|
||||
access_granted_at: string | null;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">];
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never;
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never;
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {
|
||||
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -2,6 +2,9 @@ import "react-native-url-polyfill/auto";
|
|||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { Database } from "@/lib/database.types";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import * as aesjs from "aes-js";
|
||||
import * as Crypto from "expo-crypto";
|
||||
|
||||
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL as string;
|
||||
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string;
|
||||
|
|
@ -10,9 +13,66 @@ if (!supabaseUrl || !supabaseAnonKey) {
|
|||
throw new Error("Missing Supabase environment variables");
|
||||
}
|
||||
|
||||
// As Expo's SecureStore does not support values larger than 2048
|
||||
// bytes, an AES-256 key is generated and stored in SecureStore, while
|
||||
// it is used to encrypt/decrypt values stored in AsyncStorage.
|
||||
class LargeSecureStore {
|
||||
private async _encrypt(key: string, value: string) {
|
||||
const encryptionKey = Crypto.getRandomValues(new Uint8Array(256 / 8));
|
||||
|
||||
const cipher = new aesjs.ModeOfOperation.ctr(
|
||||
encryptionKey,
|
||||
new aesjs.Counter(1)
|
||||
);
|
||||
const encryptedBytes = cipher.encrypt(aesjs.utils.utf8.toBytes(value));
|
||||
|
||||
await SecureStore.setItemAsync(
|
||||
key,
|
||||
aesjs.utils.hex.fromBytes(encryptionKey)
|
||||
);
|
||||
|
||||
return aesjs.utils.hex.fromBytes(encryptedBytes);
|
||||
}
|
||||
|
||||
private async _decrypt(key: string, value: string) {
|
||||
const encryptionKeyHex = await SecureStore.getItemAsync(key);
|
||||
if (!encryptionKeyHex) {
|
||||
return encryptionKeyHex;
|
||||
}
|
||||
|
||||
const cipher = new aesjs.ModeOfOperation.ctr(
|
||||
aesjs.utils.hex.toBytes(encryptionKeyHex),
|
||||
new aesjs.Counter(1)
|
||||
);
|
||||
const decryptedBytes = cipher.decrypt(aesjs.utils.hex.toBytes(value));
|
||||
|
||||
return aesjs.utils.utf8.fromBytes(decryptedBytes);
|
||||
}
|
||||
|
||||
async getItem(key: string) {
|
||||
const encrypted = await AsyncStorage.getItem(key);
|
||||
if (!encrypted) {
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
return await this._decrypt(key, encrypted);
|
||||
}
|
||||
|
||||
async removeItem(key: string) {
|
||||
await AsyncStorage.removeItem(key);
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
}
|
||||
|
||||
async setItem(key: string, value: string) {
|
||||
const encrypted = await this._encrypt(key, value);
|
||||
|
||||
await AsyncStorage.setItem(key, encrypted);
|
||||
}
|
||||
}
|
||||
|
||||
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
storage: AsyncStorage,
|
||||
storage: new LargeSecureStore(),
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
|
|
|
|||
2040
xtablo-expo/package-lock.json
generated
2040
xtablo-expo/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,8 +5,8 @@
|
|||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint"
|
||||
|
|
@ -21,43 +21,54 @@
|
|||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@rn-vui/base": "^5.1.3",
|
||||
"@rn-vui/themed": "^5.1.3",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@supabase/supabase-js": "^2.51.0",
|
||||
"@tanstack/react-query": "^5.75.2",
|
||||
"expo": "^53.0.0",
|
||||
"expo-av": "~15.1.4",
|
||||
"expo-blur": "~14.1.4",
|
||||
"aes-js": "^3.1.2",
|
||||
"expo": "^53.0.19",
|
||||
"expo-av": "~15.1.7",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.5",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-font": "~13.3.2",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image-manipulator": "~13.1.5",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linking": "~7.1.4",
|
||||
"expo-router": "~5.0.5",
|
||||
"expo-router": "~5.1.3",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
"expo-splash-screen": "~0.30.8",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.4",
|
||||
"expo-system-ui": "~5.0.7",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.10",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react-native": "^0.525.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native": "0.79.5",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.10.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-web": "^0.20.0",
|
||||
"react-native-webview": "13.13.5",
|
||||
"stream-chat-expo": "^6.7.3",
|
||||
"zustand": "^5.0.4"
|
||||
"zustand": "^5.0.4",
|
||||
"expo-crypto": "~14.1.5",
|
||||
"expo-auth-session": "~6.2.1",
|
||||
"expo-linear-gradient": "~14.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/aes-js": "^3.1.4",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/react": "~19.0.10",
|
||||
"@types/react-test-renderer": "^19.0.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~53.0.4",
|
||||
"jest-expo": "~53.0.9",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,40 +1,34 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { StreamChat } from "stream-chat";
|
||||
import { Chat, OverlayProvider } from "stream-chat-expo";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { Chat, OverlayProvider, useCreateChatClient } from "stream-chat-expo";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { Text } from "react-native";
|
||||
|
||||
export default function ChatProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const user = useUser();
|
||||
|
||||
const client = StreamChat.getInstance(
|
||||
process.env.EXPO_PUBLIC_STREAM_CHAT_API_KEY as string
|
||||
);
|
||||
const client = useCreateChatClient({
|
||||
apiKey: process.env.EXPO_PUBLIC_STREAM_CHAT_API_KEY as string,
|
||||
options: { timeout: 5000 },
|
||||
tokenOrProvider: user.streamToken,
|
||||
userData: {
|
||||
id: user.id,
|
||||
name: user.name ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const connect = async () => {
|
||||
await client.connectUser(
|
||||
{
|
||||
id: "artslidd",
|
||||
name: "Arthur",
|
||||
},
|
||||
client.devToken("artslidd")
|
||||
);
|
||||
setIsReady(true);
|
||||
const channel = client.channel("messaging", "hello");
|
||||
await channel.watch();
|
||||
};
|
||||
connect();
|
||||
return () => {
|
||||
client.disconnectUser();
|
||||
setIsReady(false);
|
||||
};
|
||||
}, []);
|
||||
if (!user.streamToken) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text>Chat Indisponible</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isReady) {
|
||||
if (!client) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { createStore, StoreApi, useStore } from "zustand";
|
||||
import React from "react";
|
||||
import { Tables } from "@/lib/database.types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { Tables } from "@/types/database.types";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { Redirect } from "expo-router";
|
||||
import { useGetUser } from "@/hooks/user";
|
||||
|
||||
type User = Tables<"profiles">;
|
||||
type User = Tables<"profiles"> & {
|
||||
streamToken: string | null;
|
||||
};
|
||||
|
||||
const UserStoreContext = React.createContext<StoreApi<User> | null>(null);
|
||||
|
||||
|
|
@ -15,30 +16,18 @@ export const UserStoreProvider = ({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { data, isPending } = useQuery<User | null>({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.from("profiles").select("*");
|
||||
if (error) throw error;
|
||||
return data[0];
|
||||
},
|
||||
});
|
||||
const { user, isLoading } = useGetUser();
|
||||
|
||||
const [store] = React.useState(() => {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return createStore<User>()(() => data);
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
if (!store) {
|
||||
if (!user) {
|
||||
return <Redirect href="/(auth)/login" />;
|
||||
}
|
||||
|
||||
const store = createStore<User>()(() => user);
|
||||
|
||||
return (
|
||||
<UserStoreContext.Provider value={store as StoreApi<User>}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,137 @@
|
|||
import { create } from "zustand";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { Provider, Session } from "@supabase/supabase-js";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import { makeRedirectUri } from "expo-auth-session";
|
||||
import * as QueryParams from "expo-auth-session/build/QueryParams";
|
||||
import { Linking } from "react-native";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface AuthState {
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
initialize: () => Promise<void>;
|
||||
initialize: (queryClient: QueryClient) => Promise<void>;
|
||||
setSession: (session: Session | null) => void;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
signUp: (email: string, password: string) => Promise<void>;
|
||||
signUp: (
|
||||
email: string,
|
||||
password: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
companyName: string
|
||||
) => Promise<void>;
|
||||
performOAuth: (provider: Provider) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
createSessionFromUrl: (url: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useAuth = create<AuthState>((set) => ({
|
||||
session: null,
|
||||
WebBrowser.maybeCompleteAuthSession();
|
||||
const redirectTo = makeRedirectUri({ path: "/(home)/(tabs)" });
|
||||
|
||||
export const useAuth = create<AuthState>((set, get) => ({
|
||||
loading: true,
|
||||
initialize: async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
set({ session, loading: false });
|
||||
session: null,
|
||||
setSession: (session: Session | null) => set({ session }),
|
||||
initialize: async (queryClient: QueryClient) => {
|
||||
try {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
set({
|
||||
session,
|
||||
});
|
||||
|
||||
supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
set({
|
||||
session,
|
||||
});
|
||||
});
|
||||
|
||||
const initialUrl = await Linking.getInitialURL();
|
||||
if (initialUrl) {
|
||||
await get().createSessionFromUrl(initialUrl);
|
||||
}
|
||||
|
||||
Linking.addEventListener("url", ({ url }) => {
|
||||
get().createSessionFromUrl(url);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Auth initialization error:", error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
setSession: (session) => set({ session }),
|
||||
login: async (email: string, password: string) => {
|
||||
await supabase.auth.signInWithPassword({ email, password });
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (error) throw error;
|
||||
set({ loading: false });
|
||||
},
|
||||
signUp: async (email: string, password: string) => {
|
||||
await supabase.auth.signUp({ email, password });
|
||||
signUp: async (
|
||||
email: string,
|
||||
password: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
companyName: string
|
||||
) => {
|
||||
await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
firstName,
|
||||
lastName,
|
||||
companyName,
|
||||
},
|
||||
},
|
||||
});
|
||||
set({ loading: false });
|
||||
},
|
||||
performOAuth: async (provider: Provider) => {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo,
|
||||
skipBrowserRedirect: true,
|
||||
queryParams: {
|
||||
access_type: "offline",
|
||||
prompt: "consent",
|
||||
},
|
||||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
const res = await WebBrowser.openAuthSessionAsync(
|
||||
data?.url ?? "",
|
||||
redirectTo
|
||||
);
|
||||
|
||||
if (res.type === "success") {
|
||||
const { url } = res;
|
||||
await get().createSessionFromUrl(url);
|
||||
}
|
||||
},
|
||||
signOut: async () => {
|
||||
await supabase.auth.signOut();
|
||||
set({ loading: false });
|
||||
},
|
||||
createSessionFromUrl: async (url: string) => {
|
||||
const { params, errorCode } = QueryParams.getQueryParams(url);
|
||||
|
||||
if (errorCode) throw new Error(errorCode);
|
||||
const { access_token, refresh_token } = params;
|
||||
|
||||
if (!access_token) return;
|
||||
|
||||
const { data, error } = await supabase.auth.setSession({
|
||||
access_token,
|
||||
refresh_token,
|
||||
});
|
||||
if (error) throw error;
|
||||
set({ session: data.session });
|
||||
},
|
||||
}));
|
||||
|
||||
// Set up auth state listeners
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
useAuth.getState().setSession(session);
|
||||
});
|
||||
|
||||
// Initialize auth state
|
||||
useAuth.getState().initialize();
|
||||
|
|
|
|||
498
xtablo-expo/types/database.types.ts
Normal file
498
xtablo-expo/types/database.types.ts
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instanciate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "12.2.3 (519615d)"
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
devis: {
|
||||
Row: {
|
||||
client_email: string
|
||||
created_at: string
|
||||
date: string
|
||||
due_date: string
|
||||
id: string
|
||||
items: Json
|
||||
notes: string | null
|
||||
number: string
|
||||
status: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
terms: string | null
|
||||
total: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
client_email: string
|
||||
created_at?: string
|
||||
date: string
|
||||
due_date: string
|
||||
id?: string
|
||||
items?: Json
|
||||
notes?: string | null
|
||||
number: string
|
||||
status?: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
terms?: string | null
|
||||
total: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
client_email?: string
|
||||
created_at?: string
|
||||
date?: string
|
||||
due_date?: string
|
||||
id?: string
|
||||
items?: Json
|
||||
notes?: string | null
|
||||
number?: string
|
||||
status?: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal?: number
|
||||
tax?: number
|
||||
terms?: string | null
|
||||
total?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
events: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
created_by: string
|
||||
deleted_at: string | null
|
||||
description: string | null
|
||||
end_time: string | null
|
||||
id: string
|
||||
start_date: string
|
||||
start_time: string
|
||||
tablo_id: string
|
||||
title: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
created_by: string
|
||||
deleted_at?: string | null
|
||||
description?: string | null
|
||||
end_time?: string | null
|
||||
id?: string
|
||||
start_date: string
|
||||
start_time: string
|
||||
tablo_id: string
|
||||
title: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
created_by?: string
|
||||
deleted_at?: string | null
|
||||
description?: string | null
|
||||
end_time?: string | null
|
||||
id?: string
|
||||
start_date?: string
|
||||
start_time?: string
|
||||
tablo_id?: string
|
||||
title?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_events_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "events_and_tablos"
|
||||
referencedColumns: ["tablo_id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_events_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_events_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
feedbacks: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
fd_type: string
|
||||
id: number
|
||||
message: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
fd_type: string
|
||||
id?: number
|
||||
message: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
fd_type?: string
|
||||
id?: number
|
||||
message?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
profiles: {
|
||||
Row: {
|
||||
avatar_url: string | null
|
||||
email: string | null
|
||||
id: string
|
||||
name: string | null
|
||||
}
|
||||
Insert: {
|
||||
avatar_url?: string | null
|
||||
email?: string | null
|
||||
id: string
|
||||
name?: string | null
|
||||
}
|
||||
Update: {
|
||||
avatar_url?: string | null
|
||||
email?: string | null
|
||||
id?: string
|
||||
name?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
tablo_access: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
granted_by: string
|
||||
id: number
|
||||
is_active: boolean | null
|
||||
is_admin: boolean | null
|
||||
tablo_id: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
granted_by: string
|
||||
id?: number
|
||||
is_active?: boolean | null
|
||||
is_admin?: boolean | null
|
||||
tablo_id: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
granted_by?: string
|
||||
id?: number
|
||||
is_active?: boolean | null
|
||||
is_admin?: boolean | null
|
||||
tablo_id?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "events_and_tablos"
|
||||
referencedColumns: ["tablo_id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "profiles"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: string
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: string
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
tablo_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "events_and_tablos"
|
||||
referencedColumns: ["tablo_id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
tablos: {
|
||||
Row: {
|
||||
color: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string
|
||||
image: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position: number
|
||||
status: string
|
||||
}
|
||||
Insert: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
image?: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
Update: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
image?: string | null
|
||||
name?: string
|
||||
owner_id?: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
events_and_tablos: {
|
||||
Row: {
|
||||
description: string | null
|
||||
end_time: string | null
|
||||
event_id: string | null
|
||||
start_date: string | null
|
||||
start_time: string | null
|
||||
tablo_color: string | null
|
||||
tablo_id: string | null
|
||||
tablo_name: string | null
|
||||
tablo_status: string | null
|
||||
title: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
user_tablos: {
|
||||
Row: {
|
||||
access_level: string | null
|
||||
color: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string | null
|
||||
image: string | null
|
||||
is_admin: boolean | null
|
||||
name: string | null
|
||||
position: number | null
|
||||
status: string | null
|
||||
user_id: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "profiles"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Functions: {
|
||||
generate_random_string: {
|
||||
Args: { length?: number }
|
||||
Returns: string
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
||||
|
||||
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {
|
||||
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
22
xtablo-expo/types/events.types.ts
Normal file
22
xtablo-expo/types/events.types.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Tables, TablesInsert, TablesUpdate } from "@/types/database.types";
|
||||
import { RemoveNullFromObject } from "@/types/removeNull";
|
||||
|
||||
export type Event = RemoveNullFromObject<
|
||||
Tables<"events">,
|
||||
"created_at" | "end_time"
|
||||
>;
|
||||
export type EventInsert = TablesInsert<"events">;
|
||||
export type EventUpdate = TablesUpdate<"events">;
|
||||
|
||||
export type EventAndTablo = RemoveNullFromObject<
|
||||
Tables<"events_and_tablos">,
|
||||
| "event_id"
|
||||
| "tablo_id"
|
||||
| "tablo_name"
|
||||
| "tablo_color"
|
||||
| "tablo_status"
|
||||
| "start_time"
|
||||
| "end_time"
|
||||
| "title"
|
||||
| "start_date"
|
||||
>;
|
||||
11
xtablo-expo/types/removeNull.ts
Normal file
11
xtablo-expo/types/removeNull.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Utility type to remove null from a type
|
||||
*/
|
||||
export type RemoveNull<T> = T extends null ? never : T;
|
||||
|
||||
/**
|
||||
* Utility type to remove null from all properties of an object type
|
||||
*/
|
||||
export type RemoveNullFromObject<T, K extends keyof T = keyof T> = {
|
||||
[L in keyof T]: L extends K ? RemoveNull<T[L]> : T[L];
|
||||
};
|
||||
20
xtablo-expo/types/tablos.types.ts
Normal file
20
xtablo-expo/types/tablos.types.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Database } from "@/types/database.types";
|
||||
import { RemoveNullFromObject } from "@/types/removeNull";
|
||||
|
||||
export type UserTablo = RemoveNullFromObject<
|
||||
Database["public"]["Views"]["user_tablos"]["Row"],
|
||||
| "id"
|
||||
| "access_level"
|
||||
| "is_admin"
|
||||
| "created_at"
|
||||
| "deleted_at"
|
||||
| "position"
|
||||
| "user_id"
|
||||
| "name"
|
||||
| "status"
|
||||
>;
|
||||
|
||||
export type Tablo = Database["public"]["Tables"]["tablos"];
|
||||
|
||||
export type TabloInsert = Tablo["Insert"];
|
||||
export type TabloUpdate = Tablo["Update"];
|
||||
Loading…
Reference in a new issue