From 5bbc5d257443bb670541db2ce8b5601266d99f56 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:00:42 +0000 Subject: [PATCH 1/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/integrations/supabase/types.ts | 252 +++++++++++++++++- ...8_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql | 210 +++++++++++++++ 2 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index ba7e5bb..c3e0690 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -14,7 +14,257 @@ export type Database = { } public: { Tables: { - [_ in never]: never + agent_instances: { + Row: { + container_name: string | null + created_at: string + id: string + status: string + telegram_bot_token_vault_id: string | null + telegram_bot_username: string | null + updated_at: string + user_id: string + uuid_tenant: string + vps_host: string | null + } + Insert: { + container_name?: string | null + created_at?: string + id?: string + status?: string + telegram_bot_token_vault_id?: string | null + telegram_bot_username?: string | null + updated_at?: string + user_id: string + uuid_tenant?: string + vps_host?: string | null + } + Update: { + container_name?: string | null + created_at?: string + id?: string + status?: string + telegram_bot_token_vault_id?: string | null + telegram_bot_username?: string | null + updated_at?: string + user_id?: string + uuid_tenant?: string + vps_host?: string | null + } + Relationships: [ + { + foreignKeyName: "agent_instances_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + ] + } + enterprise_leads: { + Row: { + company_name: string + contact_name: string + created_at: string + email: string + id: string + message: string | null + phone: string | null + status: string + team_size: string + } + Insert: { + company_name: string + contact_name: string + created_at?: string + email: string + id?: string + message?: string | null + phone?: string | null + status?: string + team_size: string + } + Update: { + company_name?: string + contact_name?: string + created_at?: string + email?: string + id?: string + message?: string | null + phone?: string | null + status?: string + team_size?: string + } + Relationships: [] + } + plans: { + Row: { + created_at: string + description: string | null + display_order: number + features: Json + highlighted: boolean + id: string + is_enterprise: boolean + name: string + price_monthly_brl: number | null + price_yearly_brl: number | null + slug: string + stripe_price_id_monthly: string | null + stripe_price_id_yearly: string | null + } + Insert: { + created_at?: string + description?: string | null + display_order?: number + features?: Json + highlighted?: boolean + id?: string + is_enterprise?: boolean + name: string + price_monthly_brl?: number | null + price_yearly_brl?: number | null + slug: string + stripe_price_id_monthly?: string | null + stripe_price_id_yearly?: string | null + } + Update: { + created_at?: string + description?: string | null + display_order?: number + features?: Json + highlighted?: boolean + id?: string + is_enterprise?: boolean + name?: string + price_monthly_brl?: number | null + price_yearly_brl?: number | null + slug?: string + stripe_price_id_monthly?: string | null + stripe_price_id_yearly?: string | null + } + Relationships: [] + } + profiles: { + Row: { + avatar_url: string | null + company_name: string | null + cpf_cnpj: string | null + created_at: string + full_name: string + id: string + onboarding_completed: boolean + phone: string | null + stripe_customer_id: string | null + updated_at: string + } + Insert: { + avatar_url?: string | null + company_name?: string | null + cpf_cnpj?: string | null + created_at?: string + full_name?: string + id: string + onboarding_completed?: boolean + phone?: string | null + stripe_customer_id?: string | null + updated_at?: string + } + Update: { + avatar_url?: string | null + company_name?: string | null + cpf_cnpj?: string | null + created_at?: string + full_name?: string + id?: string + onboarding_completed?: boolean + phone?: string | null + stripe_customer_id?: string | null + updated_at?: string + } + Relationships: [] + } + stripe_webhook_events: { + Row: { + event_type: string + id: string + payload: Json | null + processed_at: string + stripe_event_id: string + } + Insert: { + event_type: string + id?: string + payload?: Json | null + processed_at?: string + stripe_event_id: string + } + Update: { + event_type?: string + id?: string + payload?: Json | null + processed_at?: string + stripe_event_id?: string + } + Relationships: [] + } + subscriptions: { + Row: { + billing_cycle: string + cancel_at_period_end: boolean + created_at: string + current_period_end: string | null + current_period_start: string | null + id: string + plan_id: string | null + status: string + stripe_subscription_id: string | null + updated_at: string + user_id: string + } + Insert: { + billing_cycle: string + cancel_at_period_end?: boolean + created_at?: string + current_period_end?: string | null + current_period_start?: string | null + id?: string + plan_id?: string | null + status: string + stripe_subscription_id?: string | null + updated_at?: string + user_id: string + } + Update: { + billing_cycle?: string + cancel_at_period_end?: boolean + created_at?: string + current_period_end?: string | null + current_period_start?: string | null + id?: string + plan_id?: string | null + status?: string + stripe_subscription_id?: string | null + updated_at?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "subscriptions_plan_id_fkey" + columns: ["plan_id"] + isOneToOne: false + referencedRelation: "plans" + referencedColumns: ["id"] + }, + { + foreignKeyName: "subscriptions_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + ] + } } Views: { [_ in never]: never diff --git a/supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql b/supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql new file mode 100644 index 0000000..41fb2a2 --- /dev/null +++ b/supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql @@ -0,0 +1,210 @@ + +-- ============================================ +-- HELPER: trigger para updated_at +-- ============================================ +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER +LANGUAGE plpgsql +SET search_path = public +AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$; + +-- ============================================ +-- PROFILES +-- ============================================ +CREATE TABLE public.profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + full_name TEXT NOT NULL DEFAULT '', + company_name TEXT, + cpf_cnpj TEXT, + phone TEXT, + avatar_url TEXT, + stripe_customer_id TEXT UNIQUE, + onboarding_completed BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_profiles_stripe_customer_id ON public.profiles(stripe_customer_id); + +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Usuários veem o próprio perfil" + ON public.profiles FOR SELECT + USING (auth.uid() = id); + +CREATE POLICY "Usuários atualizam o próprio perfil" + ON public.profiles FOR UPDATE + USING (auth.uid() = id); + +CREATE POLICY "Usuários inserem o próprio perfil" + ON public.profiles FOR INSERT + WITH CHECK (auth.uid() = id); + +CREATE TRIGGER update_profiles_updated_at + BEFORE UPDATE ON public.profiles + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- Trigger: cria profile automaticamente no signup +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + INSERT INTO public.profiles (id, full_name) + VALUES ( + NEW.id, + COALESCE(NEW.raw_user_meta_data->>'full_name', '') + ); + RETURN NEW; +END; +$$; + +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); + +-- ============================================ +-- PLANS +-- ============================================ +CREATE TABLE public.plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + description TEXT, + price_monthly_brl NUMERIC(10,2), + price_yearly_brl NUMERIC(10,2), + stripe_price_id_monthly TEXT, + stripe_price_id_yearly TEXT, + features JSONB NOT NULL DEFAULT '[]'::jsonb, + highlighted BOOLEAN NOT NULL DEFAULT false, + is_enterprise BOOLEAN NOT NULL DEFAULT false, + display_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +ALTER TABLE public.plans ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Planos são públicos" + ON public.plans FOR SELECT + USING (true); + +INSERT INTO public.plans (slug, name, description, price_monthly_brl, price_yearly_brl, features, highlighted, is_enterprise, display_order) VALUES +('basic', 'Basic', 'Para quem está começando a explorar IA pessoal', 69.90, 671.04, + '["Memória persistente básica","Até 500 mensagens/mês","Integração com Telegram","Suporte por e-mail"]'::jsonb, + false, false, 1), +('starter', 'Starter', 'Para profissionais que querem produtividade real', 199.90, 1919.04, + '["Memória persistente avançada","Até 3.000 mensagens/mês","Integração Google Workspace","Skills personalizadas (até 10)","Suporte prioritário"]'::jsonb, + false, false, 2), +('professional', 'Professional', 'Para quem usa IA o dia inteiro', 399.90, 3839.04, + '["Memória persistente ilimitada","Mensagens ilimitadas","Skills ilimitadas","Agendamentos automáticos","Integrações premium","Suporte dedicado em horário comercial"]'::jsonb, + true, false, 3), +('enterprise', 'Enterprise', 'Para times e empresas', NULL, NULL, + '["Tudo do Professional","Múltiplos usuários","SSO e auditoria","SLA garantido","Onboarding personalizado","Gerente de conta dedicado"]'::jsonb, + false, true, 4); + +-- ============================================ +-- SUBSCRIPTIONS +-- ============================================ +CREATE TABLE public.subscriptions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + plan_id UUID REFERENCES public.plans(id), + stripe_subscription_id TEXT UNIQUE, + status TEXT NOT NULL CHECK (status IN ('active','trialing','past_due','canceled','incomplete','incomplete_expired','unpaid')), + billing_cycle TEXT NOT NULL CHECK (billing_cycle IN ('monthly','yearly')), + current_period_start TIMESTAMPTZ, + current_period_end TIMESTAMPTZ, + cancel_at_period_end BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_subscriptions_user_id ON public.subscriptions(user_id); +CREATE INDEX idx_subscriptions_stripe_subscription_id ON public.subscriptions(stripe_subscription_id); +CREATE INDEX idx_subscriptions_status ON public.subscriptions(status); + +ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Usuários veem a própria assinatura" + ON public.subscriptions FOR SELECT + USING (auth.uid() = user_id); + +CREATE TRIGGER update_subscriptions_updated_at + BEFORE UPDATE ON public.subscriptions + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- ============================================ +-- AGENT INSTANCES +-- ============================================ +CREATE TABLE public.agent_instances ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID UNIQUE NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'provisioning' CHECK (status IN ('provisioning','active','suspended','error')), + vps_host TEXT, + container_name TEXT, + telegram_bot_token_vault_id UUID, + telegram_bot_username TEXT, + uuid_tenant UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_agent_instances_user_id ON public.agent_instances(user_id); +CREATE INDEX idx_agent_instances_uuid_tenant ON public.agent_instances(uuid_tenant); +CREATE INDEX idx_agent_instances_status ON public.agent_instances(status); + +ALTER TABLE public.agent_instances ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Usuários veem o próprio agente" + ON public.agent_instances FOR SELECT + USING (auth.uid() = user_id); + +CREATE TRIGGER update_agent_instances_updated_at + BEFORE UPDATE ON public.agent_instances + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- ============================================ +-- STRIPE WEBHOOK EVENTS (idempotência) +-- ============================================ +CREATE TABLE public.stripe_webhook_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + stripe_event_id TEXT UNIQUE NOT NULL, + event_type TEXT NOT NULL, + processed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + payload JSONB +); + +CREATE INDEX idx_stripe_webhook_events_event_id ON public.stripe_webhook_events(stripe_event_id); +CREATE INDEX idx_stripe_webhook_events_event_type ON public.stripe_webhook_events(event_type); + +ALTER TABLE public.stripe_webhook_events ENABLE ROW LEVEL SECURITY; +-- Sem policies: apenas service_role acessa + +-- ============================================ +-- ENTERPRISE LEADS +-- ============================================ +CREATE TABLE public.enterprise_leads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + company_name TEXT NOT NULL, + contact_name TEXT NOT NULL, + email TEXT NOT NULL, + phone TEXT, + team_size TEXT NOT NULL CHECK (team_size IN ('1-10','11-50','51-200','200+')), + message TEXT, + status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','contacted','qualified','converted','lost')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +ALTER TABLE public.enterprise_leads ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Qualquer pessoa pode enviar lead" + ON public.enterprise_leads FOR INSERT + WITH CHECK (true); +-- Leitura: apenas service_role From 6048ddcfba1515095934c1d0669c2bf294badb4d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:00:58 +0000 Subject: [PATCH 2/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- ...70055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql diff --git a/supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql b/supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql new file mode 100644 index 0000000..4239d35 --- /dev/null +++ b/supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql @@ -0,0 +1,15 @@ + +DROP POLICY IF EXISTS "Qualquer pessoa pode enviar lead" ON public.enterprise_leads; + +CREATE POLICY "Visitantes podem enviar lead" + ON public.enterprise_leads FOR INSERT + TO anon, authenticated + WITH CHECK ( + char_length(company_name) BETWEEN 1 AND 200 + AND char_length(contact_name) BETWEEN 1 AND 200 + AND char_length(email) BETWEEN 3 AND 320 + AND email ~* '^[^@\s]+@[^@\s]+\.[^@\s]+$' + AND team_size IN ('1-10','11-50','51-200','200+') + AND (message IS NULL OR char_length(message) <= 2000) + AND status = 'new' + ); From 46c723c01807f970932773ae16dd0f3c5f8e5a98 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:01:19 +0000 Subject: [PATCH 3/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- package-lock.json | 440 +++++++++++++++++++++++++++--- package.json | 1 + src/integrations/lovable/index.ts | 38 +++ 3 files changed, 439 insertions(+), 40 deletions(-) create mode 100644 src/integrations/lovable/index.ts diff --git a/package-lock.json b/package-lock.json index 0b0ca41..64b0398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "dependencies": { "@cloudflare/vite-plugin": "^1.25.5", "@hookform/resolvers": "^5.2.2", + "@lovable.dev/cloud-auth-js": "^1.1.1", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", @@ -34,8 +35,9 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.103.3", "@tailwindcss/vite": "^4.2.1", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "^5.99.0", "@tanstack/react-router": "^1.168.0", "@tanstack/react-start": "^1.167.14", "@tanstack/router-plugin": "^1.167.10", @@ -44,12 +46,14 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.38.0", "input-otp": "^1.4.2", "lucide-react": "^0.575.0", "react": "^19.2.0", "react-day-picker": "^9.14.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.71.2", + "react-hook-form": "^7.72.1", + "react-imask": "^7.6.1", "react-resizable-panels": "^4.6.5", "recharts": "^2.15.4", "sonner": "^2.0.7", @@ -58,11 +62,11 @@ "tw-animate-css": "^1.3.4", "vaul": "^1.1.2", "vite-tsconfig-paths": "^6.0.2", - "zod": "^3.24.2" + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.32.0", - "@lovable.dev/vite-tanstack-config": "^1.2.0", + "@lovable.dev/vite-tanstack-config": "^1.4.0", "@types/node": "^22.16.5", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", @@ -120,7 +124,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -352,6 +355,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -426,7 +441,6 @@ "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.32.2.tgz", "integrity": "sha512-sEI/jusfDvzHIL4oiBBV5iUxAXfTRvguecIDWQ/AxBgEjGO1ZslHOEy4rlxfgqrdRWtq0TM9m3oC+hT32hajEg==", "license": "MIT", - "peer": true, "dependencies": { "@cloudflare/unenv-preset": "2.16.0", "miniflare": "4.20260410.0", @@ -1768,13 +1782,35 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lovable.dev/cloud-auth-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@lovable.dev/cloud-auth-js/-/cloud-auth-js-1.1.1.tgz", + "integrity": "sha512-80elU8dSJG6bho0Xnfj2oy53wp883nYXrG1Wy948LC/ZUaUQ0i9EGXQFmwTLOBFrWqxb6aNaOlZUvQ8BVGhjMQ==", + "license": "MIT" + }, + "node_modules/@lovable.dev/vite-plugin-hmr-gate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lovable.dev/vite-plugin-hmr-gate/-/vite-plugin-hmr-gate-1.0.0.tgz", + "integrity": "sha512-gWpiqRKwyE68ul7xlryGmoMJC3jJEWg/Pl80/Dl8h1Ajfw6VB3Quq8Mq17D5bAPL6NjxKlmA1g8GWJEjK2fDYg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": ">=5.0.0 <8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": false + } + } + }, "node_modules/@lovable.dev/vite-tanstack-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@lovable.dev/vite-tanstack-config/-/vite-tanstack-config-1.2.0.tgz", - "integrity": "sha512-vvCb3I9mCnRwg09Re1Fr7liG23kHy2luaTGkGllS1tWQOhhlTUYj8jGRCkRd7MDjxg6TxpEQdOVgiK98Rm7sMQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lovable.dev/vite-tanstack-config/-/vite-tanstack-config-1.4.0.tgz", + "integrity": "sha512-OZmCj7t77kcKHEWANz7oeILdB1Y339h7cKz34SpNMxiyna+kzf8R57DZfBzMXY71A1UcOtCJYvSmr/wfDom8ag==", "dev": true, "license": "MIT", "dependencies": { + "@lovable.dev/vite-plugin-hmr-gate": "^1.0.0", "lovable-tagger": "1.1.13" }, "peerDependencies": { @@ -3550,6 +3586,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3563,6 +3600,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3576,6 +3614,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3589,6 +3628,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3602,6 +3642,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3615,6 +3656,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3628,6 +3670,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3641,6 +3684,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3654,6 +3698,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3667,6 +3712,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3680,6 +3726,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3693,6 +3740,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3706,6 +3754,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3719,6 +3768,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3732,6 +3782,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3745,6 +3796,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3758,6 +3810,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3771,6 +3824,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3784,6 +3838,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3797,6 +3852,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3810,6 +3866,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3823,6 +3880,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3836,6 +3894,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3849,6 +3908,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3862,6 +3922,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3892,6 +3953,113 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.103.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.103.3.tgz", + "integrity": "sha512-SMDJ4vg5jLXNEHdhN4J4ujSb203WangbDw1n3VaARH0ZqM51E6lJnoUAHlpQU9N7SzP0hfgghA9IvT8c7tGRfg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.103.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.103.3.tgz", + "integrity": "sha512-A2ZHi95GIRRlN9LGOSa/zGEIPg9taR1giDI9Gkfkgrcz0YmKV8ShiAplIrKsHQFdkzKxtsO3maJF0efL+i31mg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.103.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.103.3.tgz", + "integrity": "sha512-S0k/9FJVXDeejNfQLCJwRlm4IH8Wet/HEEdBTBpX6/G2o1eU/6CjQop/hJPZIwlQkI6D/zbHH8KymuCsBgy6jA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.103.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.103.3.tgz", + "integrity": "sha512-fUvKtSXMUk1BkApVwAurWtHF4Vzbb0UB9aC/fQXrRBek7Ta3Kaora+wHf/fGwFNQs7uRz+mvjIVpzLfpR32VXA==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.0", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.103.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.103.3.tgz", + "integrity": "sha512-5bAIEubrw5keHcdKR2RTois0O1M2Ilx4UYuzOzc07G6mLGCPS/8t1nbC6Vq451pnxR3sK+rmtFHWb9CY/OPjAw==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.103.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.103.3.tgz", + "integrity": "sha512-DuPiAz5pIJsTAQCt7B6bDZrnLzlq9+/5bta/GWTsgpLn6AkuZQcmYsQHYplv4skQ8U2raKY5HASQOu4KtYq9Qw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.103.3", + "@supabase/functions-js": "2.103.3", + "@supabase/postgrest-js": "2.103.3", + "@supabase/realtime-js": "2.103.3", + "@supabase/storage-js": "2.103.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tabby_ai/hijri-converter": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", @@ -4112,6 +4280,64 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", @@ -4149,7 +4375,6 @@ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", "license": "MIT", - "peer": true, "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", @@ -4226,7 +4451,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.167.39.tgz", "integrity": "sha512-57vpXGWhrisb69mgYBlEmpNvZCKYKix4jj4vqkiInCM4b97Y3SA8fwTPRXs7L/kEXGZCkCFzMR6EDiP15a3rWg==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/react-router": "1.168.21", "@tanstack/react-start-client": "1.166.38", @@ -4403,6 +4627,15 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/router-generator/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/router-plugin": { "version": "1.167.22", "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.167.22.tgz", @@ -4458,6 +4691,15 @@ } } }, + "node_modules/@tanstack/router-plugin/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/router-utils": { "version": "1.161.6", "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.161.6.tgz", @@ -4571,6 +4813,15 @@ "node": ">=6.9.0" } }, + "node_modules/@tanstack/start-plugin-core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/start-server-core": { "version": "1.167.19", "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.167.19.tgz", @@ -4749,6 +5000,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -4762,9 +5014,7 @@ "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4773,9 +5023,8 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4784,13 +5033,21 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", @@ -4836,7 +5093,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -5120,7 +5376,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5334,7 +5589,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5571,6 +5825,17 @@ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", "license": "MIT" }, + "node_modules/core-js-pure": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5913,8 +6178,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6065,7 +6329,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6126,7 +6389,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6454,6 +6716,33 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6624,6 +6913,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6646,6 +6944,18 @@ "node": ">= 4" } }, + "node_modules/imask": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/imask/-/imask-7.6.1.tgz", + "integrity": "sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==", + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.24.4" + }, + "engines": { + "npm": ">=4.0.0" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -7362,6 +7672,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7384,6 +7709,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -7644,6 +7970,7 @@ "version": "8.5.9", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7659,7 +7986,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7818,7 +8144,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7895,7 +8220,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7927,7 +8251,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7940,7 +8263,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7952,6 +8274,22 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-imask": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.6.1.tgz", + "integrity": "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA==", + "license": "MIT", + "dependencies": { + "imask": "^7.6.1", + "prop-types": "^15.8.1" + }, + "engines": { + "npm": ">=4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -8200,6 +8538,7 @@ "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -8296,7 +8635,6 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz", "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -9144,9 +9482,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9198,7 +9535,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unenv": { @@ -9206,7 +9542,6 @@ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -9364,8 +9699,8 @@ "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9440,7 +9775,6 @@ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -9457,6 +9791,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9473,6 +9808,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9489,6 +9825,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9505,6 +9842,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9521,6 +9859,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9537,6 +9876,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9553,6 +9893,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9569,6 +9910,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9585,6 +9927,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9601,6 +9944,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9617,6 +9961,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9633,6 +9978,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9649,6 +9995,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9665,6 +10012,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9681,6 +10029,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9697,6 +10046,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9713,6 +10063,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9729,6 +10080,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9745,6 +10097,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9761,6 +10114,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9777,6 +10131,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9793,6 +10148,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9809,6 +10165,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9825,6 +10182,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9841,6 +10199,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9857,6 +10216,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9870,6 +10230,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -9986,7 +10347,6 @@ "integrity": "sha512-T/GRD6Y5vN9g4CnGmOlfST1w7bj+1IjRFvX0K7CodZPJuPVPNPGhz8Wppah0WdT6A7I8Kad3zgZ2OkDdWtENrg==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -10571,9 +10931,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index c5445e7..2bd89c9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@cloudflare/vite-plugin": "^1.25.5", "@hookform/resolvers": "^5.2.2", + "@lovable.dev/cloud-auth-js": "^1.1.1", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", diff --git a/src/integrations/lovable/index.ts b/src/integrations/lovable/index.ts new file mode 100644 index 0000000..4561eb5 --- /dev/null +++ b/src/integrations/lovable/index.ts @@ -0,0 +1,38 @@ +// This file is auto-generated by Lovable. Do not modify it. + +import { createLovableAuth } from "@lovable.dev/cloud-auth-js"; +import { supabase } from "../supabase/client"; +const lovableAuth = createLovableAuth(); + +type SignInOptions = { + redirect_uri?: string; + extraParams?: Record; +}; + +export const lovable = { + auth: { + signInWithOAuth: async (provider: "google" | "apple" | "microsoft", opts?: SignInOptions) => { + const result = await lovableAuth.signInWithOAuth(provider, { + redirect_uri: opts?.redirect_uri, + extraParams: { + ...opts?.extraParams, + }, + }); + + if (result.redirected) { + return result; + } + + if (result.error) { + return result; + } + + try { + await supabase.auth.setSession(result.tokens); + } catch (e) { + return { error: e instanceof Error ? e : new Error(String(e)) }; + } + return result; + }, + }, +}; From 0eb8689a2754111ea6f2f5ae72ad5cd801b74b0e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:01:25 +0000 Subject: [PATCH 4/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- bun.lockb | Bin 293002 -> 293445 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index 4d5c249748abefb8032cdf7127bbd152d76a56ab..7d464b6be06a92181d8b9f04cdb57e617ff649fe 100755 GIT binary patch delta 44029 zcmeHw33v|I`}WMddGR8MEr}qBeM@AKSR*22#2!mYtr3ZYL_`wAmPBo_$HB4G656V@ zLWx}!CHA0pD%FLymZ&0CeD^bRMndUN`}_aD@A|H*!!!3e_j%5~&Y3fLr{&rldT2Ap zwRU8jZL(NR0j4Dk?~TO{lg zfVQwl2|gG|5w;fYoNDC`y>`F~Vc*UJGWQA8(Tpa?R7q_9fZ(|{B}Ws%b!usFcp z1Xu~^DC}k+<>(fQkND%jioo?iC*VAw9dI0w5^{(@B7T9YC<=VUoE6)oE=U7K;|-)}?0}Vlc}RmJ@Dh-s-3O#(t;CJ=Cj&`80!aEQ z&>2`0Ncu&9dB((kH;b9@6@u>3Dn!5cL7HH*|6jxhsrnz z3HZtZPyOyVkm8zEPxrkQc4}o2@K24XC9nq2)>W_0$1Zwxx|;M>-?x@tTeYD_wI%qZ zWE&8uq2#Afpl)#+h?bsw5=hlq%3Tj=vj`v!NbRMyhu$38!A|{gG>{VX5ZR@K-v&~` zt_XWv%+R4>!zF2WSZGAZ@c6i#2BgQr%@yQvEXC}YG`*qjG0RKGfu1g*F;OwYhk>^s zAlSn~;=-hza4!k_dZD*e*e5pD{ml~gL|{4S1vk<4hrmt=>n+^RY6*~%59!z?d0kVz zsCR&%{@4>8luD!{uqaTgK$pn4YCd`cz5<@~p21EDJPkYL_)8!~{e zqx2E=5Xyr>-6_f^GCCw~2rAs{r}GR*p{|2FWpgf&8o-s7x_w|se3(o4(6}#Qr;6ML zB>lb6-lXK>t#tt-1oQ9%XMB`Jwb6{!(&E-L`lkaVoF`0=f*O07Hf* zMu)_QMi>zkLOy6>`dy89nvRw+cvUnh2yZH2!_P_lCkwoo`w%J*|l$QBjd` z@zQwMDK!gv>ve@NL!v?khNZ$zu_pkj&Le@O*9vF{><@o533LROMv)-%agit+={%B8 zgZ!}JVe#>iVZ%4}(>;y?PYL>{zwX{2b_%E@8VhBs5zDYRIcJ(FXBRAezgO6)=AT-3I9McAmhhK-7gi5d|mg+_#h4UdeG_C@I02}1`) zhDJ)#Iu=sY)_w+9ig6H-e61d=hn>RGirOXxgQeoc!Ul71SX?aZ)Tc&7>0w0y$(L=k zp38PZAJdh~$cT{m)!@m!`cS>t2gJlg4Zv7b7-^(j8 zwbheADvpgnCkbAZ5hRH|E2M3d)~~!tfXdMXH;QK;qj> z)e~m8HwI57UERpxN7rhhM>IruG#ok! zyAiQB>{N6WffS(ySO!>nCcA81IVpLz-o`?Jl>XL0s)6c2CtwjEr7vM<*w7IY472na zzC2TmRWS*n=vUH}xq8!T!1R1N11Xx&7!2^}P+!5F^1ls8opjYaEu5ms3{Wn8VW)P{9CpgiLm-9Y z1gs3a3MBs$kh1xe?w*wV?E+n4lTIWb6!s5crvN{NodTQ=q;iP@Qn~d2Qi59mZGmxN z(IaSKsN|N&H|ZM_;;ZXoV_A$o{g;ni2;hHZU5s$GWij>Xzk1}N9R8;#t^~9Y z%JDX|5Gvwe*r~*F&`PKZ!>|?}8ZSvRH|QfSRbBT%x#lj32866ofg>TXM+6age^cZ$Z@Q|>?5tPvA&vp0k zsF3*huz^wq=utw$;u7K`qvG%^6q^t>JW*P_ z%GB*Y!cL9vD%)>kYxx>11^Wrhv9YZpI#O~(NSq5I9@KA#o``BY^?5gLC#z7pa+0!3 z58@VR>L^D_i7 zu^EuUy8uL%l9Fi|7lJ|vhR5=7Xbq%1{qTj}aq7WN<5USCCCDh0Y3uZ66b`Hge#!wo z0;7Yy-U^J`kANPEQ-sq@O8)FiJs=eh&Tyy>q@iLqcqd>7L_i5}0g~NlCN&PT;bpv& zY97@y-Z@L};Co=Fx(b!_d~80ZXLtpW3U1YLJ)biLjsViYV5}~NLa!F=ZGbe{W6;-< zm3T^Ty~dMPTtet@EJ~83PT6`y!y<>HO~y&lqv3-*ozW{iJ|b*rBxd^Z!rmPE)Ze^- zl+fW}`jYF+S-oJ60IBuv1X6cF1Bw}m4p;nZJtG<5$=?^JDd8BIeue`nV*RA8Za2e$ z?8|{=fL(#)AqhwUe{ew$C|2mzy{PlUD3!QuVW$=l4)MxB z5T!?QgS`~=-ZVBRQ4aqfj?JppsQ%lrx#y`ZgUi2XUBs)?AFA`1ZZTWFsCvJMQqIga z*9(xNSzbL={)+jzsB%M=iu)Lrjr%rc@2Xk~7nYv_rtuCWvHsT z5Jn4KyCd^!qskw$91m5##j;^`WwD;BJc5M;k~Yj{1@+lp3%esrYoN;WSV~(}zRsMz zRJjJTX-8paz`T>C!~C52c@vY0dp67VR+R>2*fj3|nX!)z{ntHZL~Qw-w82s+y0Y+Xu6yP5tGnEVh|yPR2~xPqTl+ zWFOVs0&9lunteV?gZ)12x_%&YZmya?!B82>T$;7A!dMqVisr#E=|N;MKVQ{c!--TV zid2>j`%kdz?tv_%1%kj}I7stpjo~m%GYyAHkM=N2@l(xCRVAsb7DgPiX^CXRZX`Rs zr7BNgw{hRY-2GLhXmvK)KR|BJ_WP^y0%k$m`2VZ5TxPe?W-2rH_LTBO+~=_UxPQqk z9aQ-)3&Oo4TZH>umV^5tR=cBWrop@&vtH&ef6jJvROLrZ?xb4W>qwF}>fXfmcl4D9 zu{3ZVbtS2}){|^H_{!s%b7xiF#3FHj$TB*smS*)NsSDf{%@1dOLDXtfaqq;kai7WT zyQp#_7ScsEFLafpzTB^jQigre#SM`L&2Lkrz9hY;g`d*i7imoCN~y_(pKqB>H&u3H z!MKlR>A3G^w>8GSyK0%`Zup~geaH5L8-!tYfEJeQ;wxvdp!ZaZwI}-C%kYC)4!EV@ zC{I?*xr?uSg~j$zWkA|OB4ST;E4Mv{d8C|u%#Sx7GxJ$eo9SuD4gYAMoK zl6*9Go2C>`N^jLN_a#ReI>l`IsFo5m*5SwK%9RbZ03;Y1XodQRNrfy&(&RIhLizsFtmr^=4_sVuO9<-)|>(&AK0SU0^ii!f@k^Ormpwa2LPL>7ztdA0-h+Dsm+nm_1^ z5sf`VPI6e8jT?koT8;Al%=Gq zmJD!uS}2^`%x0Quam6DB^#*+q!(cm2EwTa}<%9LFXUE(|nVI$Usu+~qr>k-UmWcaI zwjcNV%rZm80xJmjO>7bFk6F$P)e?inKV?Cme7srgOx42TupWBJJ!JAM)zU5=>$aEN zrz{PeDWRa=K;}GKwHyG4+7RK@VHx03Mqowsk~`1*=BSqXBP9vFKhCPaQ11c zT$hF59>Uh(jzu!=qgms*G!3WBRn5IeOHw@Znd^@M*@mf>;4x_O2&V{(9pEcxvUH}( z&dhzDDs#GVJ33qg{h15YJad%@G3sp<{1if1H&~lmIB4Z)p&$3~j z#_ShUt%Tqnz}DbCgXJz(Ek8`uyN&Lz8cSKC%JEEIp~?rD&4(fkrNm^m^1}d2=wxGz zNDHFOyDt^W7V{Kiok4vih~+G^Wlqazkwa^lWo*$hRelOqm3y<=%Z;S1Vmn}tXMQW& zSWTr>D=o>Ivoyrc*(KcXGUs&F;y+E2I%;J|b9oxe0QVFem8+z61uey|C87t*UTNfI z0}ELtBDJxaAxW4Ol4a3773Ln_(9#`jq80~+NXtpsTES+fC6cAqOiA+B;&MLhs|=aR zj(!wi<1|a3*{nn`U1$`QkFns7DZl9-tLA63X_?>WV}EnsIcR*EX$MT9nyF$MJ($4N zk_eMNN`=(3Wwln#KIKHHZ1lxpM$BV#?@Ow4>rUxnA!_}=^IsZlMi2p2va{z z=Qd1wSUyXQuvWmN#f(NCv`IC0T4vanvmDr;!>)yEZo8angW~!araqcUSwVG0TqsOh zUQi2ZoAIca&Mt2bFgHv`b+hQs{>rd)Hh4<_o)Epc{VKy{08HI9ooz6+(R7}(+^wp)?MK3oc@9i%HJ!^aX(8g_COuP?!VXJgH>b zi$7zr2e5naxt>ugn$K-~U~_|SE79xB%%*2U^C+;**@j;J*m+4is9Ju3JpfuJT4k6$ zwjvG|9qg|}Y-NKF1z1MLMREM5BhTB>Z*$3rW69pj$D5s;F)hh07qAfIE-Csn1wUN-n-0G`SHPT8_29o#G%_v%HcuZsQv zubR8>qX$DOt@$kblxn#FI~A;6-xa=)Bs|w9m!OuS1bxBQo(_^g}N$8r{A zQ-o8LQ6DOCP_HI4%MSB3cY_U&ba;kc$b!zH5y9ROx@Ky4<|c9@lJve76D@fEV0my^ z%=|7=A5Fce+PEJT{a93)!u|QAHharkhc6Bb)}Y^%hIo?=FpRpG*o*M$!6|X zX$DTjJ(umr{W-H-Q!Sy{`m`^xl&ij$&tdaMFjj1T4__?pa-dP~jNYTM-jDFLjDU?a zOf3DFuVwv9H!K05FMp$28k{vIW_qehgpI}uV_8vNTUO+P4JTH6=IC+G=noVtW05yh z`6A1>p<3*}*87c2D{@OO*r?^n46#|}!$yTyNL#JvvFsexqMkRNWT_92fvuIMfl9IL zgN+)4{#0Ro!Dz)8Uo0wY&ESToBs^oxfsJ~*-doSWM$3Vcnw!_fmuW=|v9QrthM^z% zPiN`hsg_)DGy-D33>x(>86oGk^wpo4ZAO4|hlWum<(OQ*;j1ICyqX35fRXdE@vMbW z!R!T_FIzg_-!cUzPYQuKr}@%;@{g+JJ~&Dzn$|vFi{BN!?`Th8mN~EmX!^8=cb=u) zQZ41KN;v;XVbBQc4_h-7|3m)t_}`fF;)o(ui{^h3Mf_SsRJ7AXgoFwNk007aNbWNs4iGHTwClgc zr2jU6|I&ZbD>PyY;|s~}SNck!BEQ+S{`OA;Y@cm$yq z_gL7U0;xqlgOEpZBs2kuHv`SUqJp;u7J|KuKzqT{ktRx5RUrARE+hYBs0qRXbQ6l6 zK)MLY-AmZtgyh~3?nQxqz!E?@Q$-gc89NIM5C!RK`OUl@JA8F z0V#-hfeB>6MM%aGxKRSf2tFUR2K=f6>5RpVA{Y;(98DA+3CTDKH;QAROL=q?jS}HVeIiNbcJNUl1t++rbm=6z&C)c=K*J z6k#3mY+3I-Cs|?%d5)AO>6q|9NQx(fy&zJPKQH)#NC~y?~_B2RGvTk_A_iVJL{j;1DSk3L<$NECLuJ+zH7TE%>2= zC!~ZA7j{BQNP@soI-ewwfy|?EqX@HT zEkqW96j5Q=sm+uIQqJswROB^)q+bh2y~GWO|B@$eRMNgCls|cF3xX8e3x!TV>i>O# zbP-ZQLIp+u?O{&=;=eQzH;OO~NEuoVq&D+0ko>I$QuynEWq|vD_%D5FLjBX2eGWGA z_${zJ@QF}(3Zww2+zHJNlX*=b<;YW@FOWveHb4X+wF8p>&Ok~?7h&&_ zBn477HdP%K+HXSAK0?~S zETLBri9ZUSl5reJ%k&?F-aDW^RlI{Dg8f?ozoWjGs{T#_D5tkX4hgA(?*gj=ZO~lE zZcEKZAeE4vuoII13c_AtpkXM0`W}M!6g(mMYar}Ey&{O9fV_o5 zL!m%O9vTU3EO*#nVFQw=g4qB3g=o{Dnh7q_L;H;0Z~;qp%ZF0y_zN zK_tF2cq)mm!o46;INg(kLw6vxpk6>4gTjCmK)7%xB>NzN5rQYAfCdYDK_vYl;Hjiy zgkCI=#=22J8g#urfPpSTa+oIUgcQJZfir-Vz`11S&#cgN$ay}jbPh8t62DsD8X!$?89*wy&w(_F90gMN*+9C0deb3-6wd)EqOS#>7m9B} zDv-;G7;-7&O(Jz936OzBX z!o47pzkA@x>7mhd9>PW*=r}ehJSJCx#ODeAcftQ1q+z`*Rr?z->1_`Gm-wW&rtDo3 zKqW<2pb*1MRn!4OE17;o;v%FrHUL8YLLu66mkj^qZm1U2KiEap;(y~NCWZKq0({LM zt-(?$FkEj!t^L2r;cXuN6Hi4ujg)W^QsbRL7F>knJ`*A~L-qdmUv5UevKNW4|At+U z|8IDp;Qu-TYGms)tvBN`giy9-`&rD zcRy(tlxC!VcR&B#{rq?L^RIS6X#)Ot_Y;MH>mBwK^_BZOD86fee^XC9IsBaj(5m3y z-A}EY|K0ukclYz(-A^%w{JZ=4E}M-sZu}4HHTmZOX0E9u&zRf(SM}qrhr3i8x@7W9 z+oew)t#Tdtw^zO2O$^8T}I3$l*JPh&VtQJpke+k30ay@A!U#@45X!zz;l@;76WCaEm)1 z0^H__1V8a?f}eTquK>UB6oNba62V>Wei(3%rxM)fIRv@9@e#lSo<{I1zfJIv`(*)s zfUv#*q7o0j z0HVl65POJl0sLCTRfv~#_;sg=Zx&37jM~E1A8AMH< zMZ~BpAY871sLd0vfT(p9#5E%7^4eEHTqI)VRS+)x5)o6cf$+HoqCQW(2BOh*5O;`h z=Z&v}xJAU$>mWS&Z6X$a1ERw>AiVgZZ$PxU0pb}E4SC=V5KoBMa05hRo=3!|IUxGv zfN09s??Bjn z58?z7t-1a8AdV0*?t2g_&mv;f4MamHw?K5_skcBhx((tE5kb81Z4kGJSb7^oSALs_g+GDl@Dqsce9=!J+WZXS84*2r z;Lo@{A!5VNAcA=w5ug47qR%fNdhs>CfarM#g!LT|eR%L45Jm2S*h54=Zn+C$I}y=$ zL4@!fL`2*JQRyCtP#$>?gx!4*Cx{5+_V+;?A!6Kp5QBIY5u68NIuK(u)T;u#SmdEg@uPl(v?2t*>!BjVG?Ao@H8F@~>s45H@~5Y|sXjN`#i zKorRXv4@BxZpj0&orvf>5Gi~I5fQ(GsPsFC4|wG7AncxkI6=fDZvPa-5hBJt1u=zZ z5i#ly5H5d!NacxtfT;Bh#5E$O^V-iqTqI)VGY~WRB_gIi2jTM^#B84W97LlRAnp*6 z#v8u?af^thFXVO#Q~0zP`MvoI73=vOly>rbeLQI+!~P6*&I4ryr2#wNAcI)M^N9FV z0ntYRv4pQtK=d?$ur`5M%7aZHidcczL&S1!u>!H3h-fPi>3jzf5oQpT%pg|rNVC#I z{)q1<_?X)l0(`<_30Ctgf={_~VZa)mNU)Y?6RhL4ivZU16oL)>62V69ZUKD8Qwgvr zAjsg2ivqAHAlSlh6JSwL46v0iBG|@r2{L(Lalm$-POyXL5$xna)_`4n4Z&_MmjLYH z!32AGMhRs$-26%^HO(=2p8dS#hYQ!75zO(`pX!I!!z%jMrPecTmC z6IMHY8YrFQPMI4TC_@zUalF#BrUJG#X&L)2Q#<35d^gjt2{`lVo|=0ZrJqbunCE1{ zJrj9qC{q**S|~@%L-8$Lv6uN&e9mW%_fr(vJOp2vq4LToRW-j9CqDn2isZQ$O%{ha z@1N!^jw{W|BA?G+=ogGqE9_~=JpH&*h7JzX9|hH}))s_Ad?aq2)QTHt`Tq+Rq^YAG>v zB&eQFNu2~o$9Fsg*I97IVP8ur!W9G#|Fxr!bWn}1u0oH_XZ|X#@n1V_~tE4Wa>(Ln+_Xp}_7 zHV`B!RfKE?N!)mF_^;LPSW&FW!gFQt{lQTQr3lUuW^_2Mgy?KFh35n@4u*ax^qj%X z5gA@ef1#KXRs}KvBo*Z{;khcz)FA2FE<9I*nZ7=tYlq;f!~CV-b_%Wrxr;>a5?oDi zdj+>!aJA?oRXSfrIiiEq6i#i(0+GVKf~y0LijFQiUrl;-Ayjg7eIYofY@MifRmY;u z58uwuRysrS4TR2+Er4*yLdYV>VhA5T_ydpjg5HCE=r32?LU3f!kHI&QlON zPfevorL!MGjrstDj>@lwd<3D-TvkCof_x0A0HzW5U zxsV5tUm*`6Pat`ar;tA&&meSiZy97cWCetdJkoK>HjqF_JBS~oIiv}sDWn;M&U|`7 z>Oof=vLS<5rdDWO=$l>1t#n=};+ot)|q`4Lq}-Ils(8e|?M z36cVt2zd;hHo&?_(G8e$AU7f3LFi+E+mL&ZTnK$^a2RqBk_n+6N?%BpgIf&AhWQNSEQE#ynwMyPq4|WS51KA$dT@tahYo#AbOl1g z#U98$$bJY76u(SCx`>2 zB4hx(rlO3eL8e1yKxRTdhI|59Z6)z`7nC6LZdg|G^b1NE`$aI`g})ym^sNOAXf%LT zmDRg=PDjDXNLKQtM3gLp!YAlY9*=u5Z(kWff3NHC-`ggy-H3V9C_1ff%y_aP4; zbh7gngid^3g6WAX?=rHU#NFF2v5z?m?zK|A>^C)2Yq$3kT--xV$BtYo< zl1m8ZLxe+<>t;w-NC0G9W8C^fLLf9z(tKAJ9?Vd_56ptcrjX_kTGKc{_QGwj%s;uL zgqJRZmDNu8-3^I^gqwNg%SyOqEW)DC6F%ZIFDrvud=KLwcs&lU2O+soy#pBpSq8UQ zxDAGM7BkdG4`IPoaHc9-$qyubYK6aIAdP{HhD5@yKnC)VD@vJiP6#{}vj~w?OFN zP6lKX))v4>FWt_-P2EAUD%(7gWxlxfPR zl#;oo;K|JuQV&uGQWxR^@qoBNXw0XPtO0~_)fmzU;tgpC@qtk3P{~okTR^CksC23( z!O#Xmxo-ujk1C>E`$GaCG})*S^4tzWp2(dh9I7I!o?eii5UM)jsM_8K(g;L6jr`<~ zhTSd@Pnd&%q?bfD8jHI_NV$h_Fg%k-(lG)m7?}~U5h=A3a_A;};}7=$_s*#|5}!fX(~0U3~TvkbTp z@(^YYq+Bh5EP^bCko$*_rI0#jxAHt67_qlxKuL^xaDPj%8 z_~Yt|+}GU||IkJa75B6mEQy$Ui#CuXV3CA8uN4nE_dX+01nhJ9ijJL`1kUp=)5PI z9^z;qP2~5W;6>#`sk{CmjzPd*R&r)oM7VoO#!m=6J$tsb&zMa+4Hq|Q0=90E2;;|$)WXqq1~hTZ7GWcm zTe%+;@EWcc=^4NBH5jv)vxFbaJe# z$c4aIz!*Qa)^_XK#s|Nc)=818371y<+7o4vLvL8Ib&))C@W^>r!l#r+*o|Dh5wtXn z_sc`%_CO z5CS$zpHe_Op@*HFMhU;c-33cI#3&yZT<_eLvw287%iJ->>U9XHBv8sGB_sa?fWhMCG+HAIKvxkQ}c zA+t?ox#M%ijCYQwgySRb`y3^2{3@umZL8bMHjX%KCA+zLG<1b;10udQMUmDea<%{!aJN)v4Gk)N7 zK>0r`B^}E?%6GZI`@k#Sb~+E&BYMu$KqxjAzW#+`C)eaL!l5buh9WV3WOe`dX`hy{ zRUhXEW&G~Mkw?jnV?XN7@?FNvcQM(^vv{ru%6EPU>}}_dG1)s9KUJDlsP3d<$)~Dg z-`UmEg9>}x{Lf7GUdB(GhDBWZDfPSS=knc+A3wEsIn(violAD+yBweY3_gt?OLd8; zX%3!gzL4*3{G4i%RhOzy(xztTyEt;ZI<fZ0Z+x{dmU!ez2f&$*SEs{;1 zdh;q@89kjI7^pLK@Bhn--{*upDkb;zbQ(L>sc(Y>E-Uacfja;k(lHp(pt$wLFAs`V zIEw5xaP@Yhmb-^{f`Yg4lcv6fgTjYT%^H}mVEpLmd5>K$W3)yM7Dq%!$>Yq+BciO9kxqBByNvox;i z*0U?t=PMXL-MT32dgl1$``YHa6sf?6nkW$%x1Hd(Va*MVJf5Bo$k$MLIs%s?_%48h z@nf+izs+{9_s6k6^7U5odrf$JNGaJzbj&pYaV% zp!aWw|F1hd-%!}p5GN zB>*kC><@_G%ZZHRecm_qbC_IP|Lb^vPRnFoHxM&L`=X{Qc=;An6n>8L)S^gM4&Otz zTz;XbskP;gI#@L!7sGhjMLPg^j zbG!5~dAE!08l_;0z$Aeco2@IqQQXu@){^8|)JU_SErz`>&uwmMEwo;;TJHN*HI&|v zQBefC_m}?kQdjgyJa~)Jrh4xXh+gEc_^S4wqyBSFv>F$Im2&5EN}Kc~{HF@@d_DWG zwxL%-w8PYDUE0*|9TKP~!KeW*BS*$hD$n`x;&uBu2j`WNyJ8xkf$z`7+?D&3F*)Nk zaKAF94tQm|p$x_-@!P-2AKlCRe$3q1?HGI9TpPF|kh44o3TSIj%b4o+xCe$>%AbQZ zT#R28c5OJvtKFJz0}+xZnh#du#_tZ-__3#JgJu)(ClNFk^uRsSanEw5B{)xTsvMesIF>LpdJf~2%457XemvP_ z-_}Zl>Lz(Or=z2$$)6NO@BSuZfNISIl0&mmeDH zQ9MVH(X+J6_@(7pv5p}VtQRKay9Dz)h}FUP<>bf7&)al9-v2k`zyrmI3DuuF+al>5 zd9JOg9PeyvvXXo9?zX17K1S#JbCDV@#_uSXe_o~azR%9iLZ}U0ycYO4f@t1tHovF1${TA9Q zus2k8eO}leZKyzQcuRXz1;y;g``Tl`V|=8&sjelcrM|w{#@E>+eP8lJ_87LV@H~4A z4<6jNg2}#`G1UG!)rO1GnvbnuvQvt;<_jvAJQTau{AdMJ9iO))udHmi6mPA)JWPK4 zphI+=_s5^3;M_f2Jy8-}xVr;V)StI=Ksv_-=tE~0o&?s*_=#|}SB^C4*yMTSO@At- zt#x_|d+k{f!|rW1&=j`v?23qgKhLghs>D59 zP33V0FxwlGP@zEHq>?EdhtAR~ne5BZ-Y#X~$5zK3yvi=AUrKi4C*a`FI8g7}2|ybt z%(~WpQpp}S50u6EB)Kxue3tvwLloa}pUOr|_Iy}n499u!QudA6HC%+>>HJbCGSJAT#CR6#sbRWyD~`+fQI-GK+@ zHbrr{p~Yk9DAJMFb3&7JS*+Sa0-p@S4843yf7%8;mFJSXB zep1`2r$@IFh1SwzJwm4O!1$5vI`!N9@uXMT2U`7Vk9E1c1Db{rHb!A=#c8bts|x38 z7(6yrFH(h>0SNMCtYhg47SoEqh=b&lf0lr@fyGV+;;1W8`pc^z_^N0%7Y&I zwRF?EscAK|vp1)l^t6_3M9iJ4o2>ZCYN+E#zUS>8@$iqvp9=+%V$CDY2U5bcuJMff zR!3dDeR=)%6zOH4h3H`X{`!(~nLkySw0j8hhiT2#-9v9Do(})10K7tb>JKsv#?D;$ zVC?4WMxA*}#?PAvkN#-SZ{@CU%y(JE`_y=sLeqx={zVN;ECo`nh|Yo|U3oPy=pNv{ zHBC)%Pz__qs|6O%KdT9OZdw`*_)z z<=gZeP!k!?lb|3^8wovhA9M*1<0siY=Z22nzjOtb2fcf1 zM`n@}xI=A3Vf==Brrn{dr%ylsPV!ro9+49SUJQs|3fXWm6BIZh5ebf^7hIZ+GmHY~+r0OoJTWGG<^#{)ng7L2k7I zv&R{|0yeMLhh9C{My?+286o;aY1eC}n!ITAAG(XTt7oGV+@~(W`++aTSdR3?!loz# z_;^>0X_dHRL$HnbzPgyMjj{I6185$1td~E4YjZ??AZz*TdZxNqP3@{@iqR(!F^|~e zaBn}_EYVc#`CJ#RGj1SC3`u(NITQ}l_w$mU@3nRNcrrHSejljUU4aS7;m><}RIC^z zJj7IGTSN>ZlL!o^nO*Ss&x7*fvY9Z)zad;9KkCu{j2DTcff*;1M>r($^jo z%>qOJSiam1NuACcxI3=s+A62V1`&Fb20S@9Z?pT4vLZK9r zH0zUbWuyHY--QB}yEHwd@I6qFXYgDEgbm4J9z8;u$G|%?@6A#qJYQEnC3Ig^-)2OVevEQ#AFIzs#f{0-%0t_ah=L2PwX;5J zzH?x=W=`~kh{lRFvTd}!;>xqGvb9``755dna5Qhxz~qD^hBQDPUPk^|1JfWqE9JS1 z7h+j&7j8-VQbl>rUff3YW`uP7Ye-ltw`=Aun?^fs0D`HP++Rx)o7R`f(YbYqQW zG+fbN3iivFy(g+6>gzh6(hzI@|0V&i=8IQpWGej*3$VGoMG-`_bq zUtt)ZO<^-AP~=m8?)J&&pR~u$v=R9hxH!Y*>+VaBMs)ZoE8pcDzXq?~51>#L3Rz9Z z^><5{(luY9T%7K8k@b7_``i3lI^V^ZG7t0CP0&dz7t)*b`0WXCM;d+YL1VMNC#+2d z)Ks9mZJX~B&d0$kwy$_o^pw;57};|8_f0V>`0;x~U~9)aG&80A z<&KruqdLTceBj|-wyLi1J3gj&*}BoUHF$qNQ@}gy(Y&_Nvx58jqB(3EuV=q_(Xi$f zBOD)Vb0NK;z^}Fz+<$v@3q|bo*f|`7cWSY_AAg*ecsUqv0>!hNbdzVa(K1}S#XIe_ zyuKOql;^d;MuV~E^7ryOlxO*&&Q|hge#qmFWW9B)=~OBE!?|a6VQ9tbf}ZG)a`%=< zqyVvLBz7D(@$^=3(<8=&eFB;ee|~AFrhTD*HU z()io=+Fu#RjLxBNwJZ8|sh!wze?9-&u=d(U{5$0DwPEYkT)f&=^=|N5yE0Ps%I5St z#QVnP^~M;)*dVg~x2s!gx#i5Xp8F>?{~x!;U$)~n=IEX4`n6*HtJf>O1r; zk+jdkKWUin z5X)b+$X5oM*Bjs8oi$$_C*D|xe>X@m;k?oiUTueeqs6@`eQ(UO{r|);_U4fPX9g}S zqYNruou;49I=tIw;JJ*5^Yqy2PS5w^#pXM|TztS^3@9)i>l5C)4}1R?+pfc(-&knN zL##B#Nb%pTqa5h8FdbYgbm{ts(+8FtA(q=7ICc20>q4=>dWW~4rDyW>gK(;29bVX% zM5HY~b#-5oyTT3UTd;VhJ^O9^n?ZQVae1b`yLaP|l+s~Q<2ef6&lii(7GBjO@UV23 z$3>W&9fD`++ip)z7Ok_W<2J29;L#EdLJvntn8lAqm^>=JAw0x@@6wW!XK}koQyr}6 zD#hV-f+EjCnB=e;Xb+sWZ%3&sS8q-#D=+3FBTddpMP}J0(|9p)!xBtfqsV0LF!!P)QWA^S|Hlq8y(Lo%Y&j!jul%#$rfo?v#bA4t0shx zpfg>b4d|(<#{xbh3a<#G7AzPBn7)920V_U(J{E%*5AqCxZ@G0eHc#=IVSJ3quA&py z%V*pEm7BdwfB!KZtHR4#RDN^Ldq*Rf_c?cirZSfEIk0+aO4SBt2z6Y}HV z1Q&XkAM?wA`^7(4vohc1D}D`;I$VVUJ(Vsl?rgCN_R$X4=}CXcZH6LMMHljVLy@Y= z3-z<6_j+ENU!v_++x(CkE#&>7;oTV;HVEp*{&LSJUX1uIUn2xAl!{$9=bx`P$9j9d z%gBX%Bm6qdfClBD-AK7@(63iZT39xCQ$ZE$VKeT%oSj_;zG6JO$+UHRRvd=Hl|1bOysq5~4By2 z`}l=s1lx@Wrqytv2wJBhX7D;gNzKn%DSAhrwMO;1Oplskjp0f;vcc zRKD#SboVZfU1&jK6dS*RR~(64&)mShN8+vRS^mLDB=#nE9EBX*;n^brkNM98g4DtL z4#K3-xzPfbdCmt)XDRY{I=+L9-1tn-K=8i1<`1vmsfE~R0niW!9yalzqYzuGO?>Jo zq&$2x-%a+|&HVf*Q+tQ-E&7mLe|7UOewZ|Xj_Zi!(qrzEXzHLuf6iwlVw7FR{YGPm z-jRqQ+J2j!xrOsu96YzZryV?bp^GEidWqWc;-j%&y%uV8qTVV~R}cBp`H;HMp3ciO zfI8Bx1xjz}5w<;#AC0~9ulelJsKQV9HBb)ycIe6LSpUnF_D2tt)x2t}xmDa|jLEZN zwVnEOHXyc|Pm5wD=?ytf@=}wC+sXTnK^?uNa-nexekpwuea|_xJ$*zKevvL^7vDGr zb@QA*9fNAnT32~99P}pZfkQ5#V@*qx*?Sj^L&JT0WiR0!#$g+{**-m$>zsS8{C(1k zYKVy%E&>dM3$>tMo(#CwuS}5!S_UuLeWm%6#(sUR zRszkQI!oN{?&~%xCFuZs16Le@$HyLD@+fp={5cTkB*dWi>_7GB>Ie8;XgR0{^eyz# zXAgecua_hK2!t^t2>%fWc&QYe)R>ckS;<=JS0`h6 zZVW)LCPsN<9_^F@X|4DA5T*}Zc81rAH4f`bxU|dz2O_JuPtDJ`J_hE8)_($WZuDzC z%XUhKb$ru=mjmVN>3nrKwc~9+KoG(!AOC@=v`8KPl*+mfOrAxzAJKnxx^ou4^#S_v zpe$Z-qN$YqPF%N)%)7$O@0$QHcO}T88i~0Yj)?^6H(i3j`3fh z*lFl7{UA^cr}7OQ$9Bi73p&U_uk!!=ns)(oY2aA_qyc(zVDomX9!`Cx`*FkCY%}jV z2``fNLBSOYZ$4|`h5>0SPoIQTj5yABPeO8M9@i&_;&WSfvzd4iU9b+;33RRE_n@es za^LWU zV2Y`3(VK#HJI$X=F|}7(oaO;jF>JIwt=E;i??=_geqd7p#fh~x)zwALref^vdYTWL zf%7exh*t1)@YCEa71m#QU@GRN*wcJGtX@e7kt)hzP0}|Tf7~h|L`pOI z{BpR^f}m60fvvl(1}s+OR4}yM*vyZmqL{wm-=~^7RD8?%7o^E6n|GXsnWK3&kC!0Nu5k3Rf$Nj<-@a?iF!^ONO#hF6|$s#A3^ z{L(P^_AoN|km(pMcAnLXc-7z$35m~p;ZclEmeBV)NBD9m;BWoyn2r)D{x!cl9aZo% zcb=i;uIUW0dVwY@A6(!MX5fj>_M*O0{y23-)jAzlHWvwV!`Ge{d5f8*Wc`4ar$f#q z{T=@Ff%7tUU(WhQi&i_dn#)VhLIf9i?OBLGRFe{anU9)fYG3is{%Cf>DnRnO!f&8L zy;82|Q+HFxHho9WZQBkZ(-R5`{EdrYFFJ~(Er!b$(#+b5SeUgnF}*7sGaKukM_5c# zmAkcE+%L0GNkv-&YJMz5_@>#2`;8w1X&P@_A8Rh!`dGxLEs}*gD(zULd3DG==WG6BN7#L4lHcq~Lc%n!+0w(wd95kS@zJ(oD9cjUL{H zVbs9YW*)zsW@^Qq=9=7+O4rca_QXbB$K_Eao{t*zj*#4US*jykc&VS$Ydl zejmTB!m(0c*}_gs$D~IV)1;9XqF;B94~dK#898*I+Urb>yLB^aYJZXn`uw?P%cxE5 zO277{TCZ-eE5s#)4iAY7leQfiR%}U!a>FlRBq)Xeu5=&sQIB;__TR!zoiW9>dx;li z8(hB&dnwotHSX8dcSMs5qvq+kT$-3!hnf6kyQr8EAp@eqTnC1Ys2>^?lQ7UFBq2V+ mWpL(@d8S&^dAm|p&1=0)C$7HBD}H#atyRs;)s9v-d;TBQ4&yTb delta 43652 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 H((C^NA|kw- From fa8bee0e36dcf06a3bbb9e8dc2acc9decf843b91 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:02:12 +0000 Subject: [PATCH 5/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/AuthCard.tsx | 38 +++++++++++ src/components/mika/GoogleButton.tsx | 55 +++++++++++++++ src/components/mika/PasswordStrengthMeter.tsx | 36 ++++++++++ src/hooks/use-auth.ts | 34 ++++++++++ src/hooks/use-profile.ts | 68 +++++++++++++++++++ src/lib/auth-errors.ts | 26 +++++++ src/lib/password.ts | 14 ++++ 7 files changed, 271 insertions(+) create mode 100644 src/components/mika/AuthCard.tsx create mode 100644 src/components/mika/GoogleButton.tsx create mode 100644 src/components/mika/PasswordStrengthMeter.tsx create mode 100644 src/hooks/use-auth.ts create mode 100644 src/hooks/use-profile.ts create mode 100644 src/lib/auth-errors.ts create mode 100644 src/lib/password.ts diff --git a/src/components/mika/AuthCard.tsx b/src/components/mika/AuthCard.tsx new file mode 100644 index 0000000..12fcb0b --- /dev/null +++ b/src/components/mika/AuthCard.tsx @@ -0,0 +1,38 @@ +import { Link } from "@tanstack/react-router"; +import { Logo } from "./Logo"; +import { ThemeToggle } from "./ThemeToggle"; + +export function AuthCard({ + title, + subtitle, + children, + footer, +}: { + title: string; + subtitle?: string; + children: React.ReactNode; + footer?: React.ReactNode; +}) { + return ( +
+
+ + + + +
+
+
+
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {children} +
+ {footer &&
{footer}
} +
+
+
+ ); +} diff --git a/src/components/mika/GoogleButton.tsx b/src/components/mika/GoogleButton.tsx new file mode 100644 index 0000000..59c87b7 --- /dev/null +++ b/src/components/mika/GoogleButton.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { lovable } from "@/integrations/lovable/index"; +import { translateAuthError } from "@/lib/auth-errors"; + +export function GoogleButton({ redirectTo }: { redirectTo?: string }) { + const [loading, setLoading] = useState(false); + + const onClick = async () => { + setLoading(true); + try { + const url = redirectTo + ? `${window.location.origin}${redirectTo}` + : window.location.origin; + const result = await lovable.auth.signInWithOAuth("google", { redirect_uri: url }); + if (result.error) { + toast.error(translateAuthError(result.error.message)); + setLoading(false); + return; + } + if (result.redirected) return; + // tokens recebidos: redireciona + window.location.href = url; + } catch (e) { + toast.error(translateAuthError(e instanceof Error ? e.message : String(e))); + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/src/components/mika/PasswordStrengthMeter.tsx b/src/components/mika/PasswordStrengthMeter.tsx new file mode 100644 index 0000000..1c5fe85 --- /dev/null +++ b/src/components/mika/PasswordStrengthMeter.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { passwordStrength, passwordStrengthLabel } from "@/lib/password"; +import { cn } from "@/lib/utils"; + +export function PasswordStrengthMeter({ password }: { password: string }) { + if (!password) return null; + const score = passwordStrength(password); + const colors = [ + "bg-destructive", + "bg-destructive", + "bg-warning", + "bg-success", + ]; + return ( +
+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+

+ Força: {passwordStrengthLabel(score)} +

+
+ ); +} diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts new file mode 100644 index 0000000..6522992 --- /dev/null +++ b/src/hooks/use-auth.ts @@ -0,0 +1,34 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { Session, User } from "@supabase/supabase-js"; +import { supabase } from "@/integrations/supabase/client"; + +interface AuthState { + user: User | null; + session: Session | null; + loading: boolean; +} + +export function useAuth(): AuthState { + const [state, setState] = useState({ + user: null, + session: null, + loading: true, + }); + + useEffect(() => { + // CRITICAL: subscribe BEFORE getSession (avoid missed events) + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + setState({ user: session?.user ?? null, session, loading: false }); + }); + + supabase.auth.getSession().then(({ data: { session } }) => { + setState({ user: session?.user ?? null, session, loading: false }); + }); + + return () => subscription.unsubscribe(); + }, []); + + return state; +} diff --git a/src/hooks/use-profile.ts b/src/hooks/use-profile.ts new file mode 100644 index 0000000..7855eec --- /dev/null +++ b/src/hooks/use-profile.ts @@ -0,0 +1,68 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "./use-auth"; + +export interface Profile { + id: string; + full_name: string; + company_name: string | null; + cpf_cnpj: string | null; + phone: string | null; + avatar_url: string | null; + stripe_customer_id: string | null; + onboarding_completed: boolean; +} + +export function useProfile() { + const { user } = useAuth(); + + return useQuery({ + queryKey: ["profile", user?.id], + enabled: !!user, + queryFn: async (): Promise => { + if (!user) return null; + const { data, error } = await supabase + .from("profiles") + .select("*") + .eq("id", user.id) + .maybeSingle(); + if (error) throw error; + return data as Profile | null; + }, + }); +} + +export interface SubscriptionRow { + id: string; + user_id: string; + plan_id: string | null; + stripe_subscription_id: string | null; + status: "active" | "trialing" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "unpaid"; + billing_cycle: "monthly" | "yearly"; + current_period_start: string | null; + current_period_end: string | null; + cancel_at_period_end: boolean; +} + +export function useSubscription() { + const { user } = useAuth(); + + return useQuery({ + queryKey: ["subscription", user?.id], + enabled: !!user, + queryFn: async (): Promise => { + if (!user) return null; + const { data, error } = await supabase + .from("subscriptions") + .select("*") + .eq("user_id", user.id) + .order("created_at", { ascending: false }) + .limit(1) + .maybeSingle(); + if (error) throw error; + return data as SubscriptionRow | null; + }, + }); +} diff --git a/src/lib/auth-errors.ts b/src/lib/auth-errors.ts new file mode 100644 index 0000000..27c8c0a --- /dev/null +++ b/src/lib/auth-errors.ts @@ -0,0 +1,26 @@ +/** + * Traduz mensagens de erro do Supabase Auth para mensagens em português específicas. + */ +export function translateAuthError(message: string | undefined): string { + if (!message) return "Algo deu errado. Tente novamente."; + const m = message.toLowerCase(); + + if (m.includes("invalid login credentials") || m.includes("invalid_credentials")) + return "E-mail ou senha incorretos. Tente novamente."; + if (m.includes("user already registered") || m.includes("already been registered") || m.includes("already exists")) + return "Esse e-mail já possui cadastro. Deseja fazer login?"; + if (m.includes("password") && (m.includes("short") || m.includes("weak") || m.includes("characters"))) + return "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número."; + if (m.includes("email") && m.includes("invalid")) + return "Formato de e-mail inválido."; + if (m.includes("rate limit") || m.includes("too many requests")) + return "Muitas tentativas. Aguarde 1 minuto e tente novamente."; + if (m.includes("expired") || m.includes("jwt") || m.includes("session")) + return "Sua sessão expirou. Faça login novamente."; + if (m.includes("email not confirmed")) + return "Confirme seu e-mail antes de entrar. Verifique sua caixa de entrada."; + if (m.includes("pwned") || m.includes("compromised") || m.includes("breach")) + return "Esta senha apareceu em vazamentos públicos. Escolha outra mais forte."; + + return message; +} diff --git a/src/lib/password.ts b/src/lib/password.ts new file mode 100644 index 0000000..58ffa5f --- /dev/null +++ b/src/lib/password.ts @@ -0,0 +1,14 @@ +export type PasswordStrength = 0 | 1 | 2 | 3; + +export function passwordStrength(password: string): PasswordStrength { + let score = 0; + if (password.length >= 8) score++; + if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++; + if (/\d/.test(password) && /[^A-Za-z0-9]/.test(password)) score++; + if (password.length >= 12) score = Math.min(3, score + 1) as PasswordStrength; + return Math.min(3, score) as PasswordStrength; +} + +export function passwordStrengthLabel(s: PasswordStrength): string { + return ["Muito fraca", "Fraca", "Média", "Forte"][s]; +} From 104dd8af7e5fa52c1aceb515254069105a7581ea Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:05:05 +0000 Subject: [PATCH 6/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/SubscriptionBanner.tsx | 112 ++++++++++ src/routeTree.gen.ts | 158 +++++++++++++- src/router.tsx | 24 ++- src/routes/__root.tsx | 27 ++- src/routes/login.tsx | 111 ++++++++-- src/routes/painel.configuracoes.tsx | 159 ++++++++++++++ src/routes/painel.faturamento.tsx | 127 +++++++++++ src/routes/painel.index.tsx | 132 ++++++++++++ src/routes/painel.tsx | 237 +++++++++++++++++++++ src/routes/recuperar-senha.tsx | 86 ++++++++ src/routes/redefinir-senha.tsx | 90 ++++++++ src/routes/signup.tsx | 215 +++++++++++++++++-- 12 files changed, 1428 insertions(+), 50 deletions(-) create mode 100644 src/components/mika/SubscriptionBanner.tsx create mode 100644 src/routes/painel.configuracoes.tsx create mode 100644 src/routes/painel.faturamento.tsx create mode 100644 src/routes/painel.index.tsx create mode 100644 src/routes/painel.tsx create mode 100644 src/routes/recuperar-senha.tsx create mode 100644 src/routes/redefinir-senha.tsx diff --git a/src/components/mika/SubscriptionBanner.tsx b/src/components/mika/SubscriptionBanner.tsx new file mode 100644 index 0000000..f45b318 --- /dev/null +++ b/src/components/mika/SubscriptionBanner.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { format } from "date-fns"; +import { ptBR } from "date-fns/locale"; +import { AlertCircle, AlertTriangle, Info } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import type { SubscriptionRow } from "@/hooks/use-profile"; + +export function SubscriptionBanner({ subscription }: { subscription: SubscriptionRow | null }) { + if (!subscription) return null; + + const periodEnd = subscription.current_period_end ? new Date(subscription.current_period_end) : null; + const periodEndPassed = periodEnd && periodEnd.getTime() < Date.now(); + + // Pagamento pendente + if (subscription.status === "past_due") { + return ( + + Atualizar pagamento + + } + /> + ); + } + + // Cancelamento agendado + if (subscription.cancel_at_period_end && periodEnd && !periodEndPassed) { + return ( + + Reativar assinatura + + } + /> + ); + } + + // Cancelada e período já passou + if (subscription.status === "canceled" && periodEndPassed) { + return ( + + Assinar novamente + + } + /> + ); + } + + return null; +} + +function Banner({ + tone, + icon: Icon, + title, + description, + action, +}: { + tone: "destructive" | "warning" | "muted"; + icon: React.ComponentType<{ className?: string }>; + title: string; + description: string; + action: React.ReactNode; +}) { + const tones = { + destructive: "bg-destructive/10 border-destructive/30 text-destructive", + warning: "bg-warning/10 border-warning/30 text-warning", + muted: "bg-muted border-border text-foreground", + }; + return ( +
+
+ +
+

{title}

+

+ {description} +

+
+
+
{action}
+
+ ); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 05e7907..dbc2b0c 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,14 +10,35 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SignupRouteImport } from './routes/signup' +import { Route as RedefinirSenhaRouteImport } from './routes/redefinir-senha' +import { Route as RecuperarSenhaRouteImport } from './routes/recuperar-senha' +import { Route as PainelRouteImport } from './routes/painel' import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' +import { Route as PainelIndexRouteImport } from './routes/painel.index' +import { Route as PainelFaturamentoRouteImport } from './routes/painel.faturamento' +import { Route as PainelConfiguracoesRouteImport } from './routes/painel.configuracoes' const SignupRoute = SignupRouteImport.update({ id: '/signup', path: '/signup', getParentRoute: () => rootRouteImport, } as any) +const RedefinirSenhaRoute = RedefinirSenhaRouteImport.update({ + id: '/redefinir-senha', + path: '/redefinir-senha', + getParentRoute: () => rootRouteImport, +} as any) +const RecuperarSenhaRoute = RecuperarSenhaRouteImport.update({ + id: '/recuperar-senha', + path: '/recuperar-senha', + getParentRoute: () => rootRouteImport, +} as any) +const PainelRoute = PainelRouteImport.update({ + id: '/painel', + path: '/painel', + getParentRoute: () => rootRouteImport, +} as any) const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', @@ -28,34 +49,96 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const PainelIndexRoute = PainelIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => PainelRoute, +} as any) +const PainelFaturamentoRoute = PainelFaturamentoRouteImport.update({ + id: '/faturamento', + path: '/faturamento', + getParentRoute: () => PainelRoute, +} as any) +const PainelConfiguracoesRoute = PainelConfiguracoesRouteImport.update({ + id: '/configuracoes', + path: '/configuracoes', + getParentRoute: () => PainelRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute + '/painel': typeof PainelRouteWithChildren + '/recuperar-senha': typeof RecuperarSenhaRoute + '/redefinir-senha': typeof RedefinirSenhaRoute '/signup': typeof SignupRoute + '/painel/configuracoes': typeof PainelConfiguracoesRoute + '/painel/faturamento': typeof PainelFaturamentoRoute + '/painel/': typeof PainelIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute + '/recuperar-senha': typeof RecuperarSenhaRoute + '/redefinir-senha': typeof RedefinirSenhaRoute '/signup': typeof SignupRoute + '/painel/configuracoes': typeof PainelConfiguracoesRoute + '/painel/faturamento': typeof PainelFaturamentoRoute + '/painel': typeof PainelIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/login': typeof LoginRoute + '/painel': typeof PainelRouteWithChildren + '/recuperar-senha': typeof RecuperarSenhaRoute + '/redefinir-senha': typeof RedefinirSenhaRoute '/signup': typeof SignupRoute + '/painel/configuracoes': typeof PainelConfiguracoesRoute + '/painel/faturamento': typeof PainelFaturamentoRoute + '/painel/': typeof PainelIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/login' | '/signup' + fullPaths: + | '/' + | '/login' + | '/painel' + | '/recuperar-senha' + | '/redefinir-senha' + | '/signup' + | '/painel/configuracoes' + | '/painel/faturamento' + | '/painel/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/signup' - id: '__root__' | '/' | '/login' | '/signup' + to: + | '/' + | '/login' + | '/recuperar-senha' + | '/redefinir-senha' + | '/signup' + | '/painel/configuracoes' + | '/painel/faturamento' + | '/painel' + id: + | '__root__' + | '/' + | '/login' + | '/painel' + | '/recuperar-senha' + | '/redefinir-senha' + | '/signup' + | '/painel/configuracoes' + | '/painel/faturamento' + | '/painel/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginRoute: typeof LoginRoute + PainelRoute: typeof PainelRouteWithChildren + RecuperarSenhaRoute: typeof RecuperarSenhaRoute + RedefinirSenhaRoute: typeof RedefinirSenhaRoute SignupRoute: typeof SignupRoute } @@ -68,6 +151,27 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SignupRouteImport parentRoute: typeof rootRouteImport } + '/redefinir-senha': { + id: '/redefinir-senha' + path: '/redefinir-senha' + fullPath: '/redefinir-senha' + preLoaderRoute: typeof RedefinirSenhaRouteImport + parentRoute: typeof rootRouteImport + } + '/recuperar-senha': { + id: '/recuperar-senha' + path: '/recuperar-senha' + fullPath: '/recuperar-senha' + preLoaderRoute: typeof RecuperarSenhaRouteImport + parentRoute: typeof rootRouteImport + } + '/painel': { + id: '/painel' + path: '/painel' + fullPath: '/painel' + preLoaderRoute: typeof PainelRouteImport + parentRoute: typeof rootRouteImport + } '/login': { id: '/login' path: '/login' @@ -82,14 +186,62 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/painel/': { + id: '/painel/' + path: '/' + fullPath: '/painel/' + preLoaderRoute: typeof PainelIndexRouteImport + parentRoute: typeof PainelRoute + } + '/painel/faturamento': { + id: '/painel/faturamento' + path: '/faturamento' + fullPath: '/painel/faturamento' + preLoaderRoute: typeof PainelFaturamentoRouteImport + parentRoute: typeof PainelRoute + } + '/painel/configuracoes': { + id: '/painel/configuracoes' + path: '/configuracoes' + fullPath: '/painel/configuracoes' + preLoaderRoute: typeof PainelConfiguracoesRouteImport + parentRoute: typeof PainelRoute + } } } +interface PainelRouteChildren { + PainelConfiguracoesRoute: typeof PainelConfiguracoesRoute + PainelFaturamentoRoute: typeof PainelFaturamentoRoute + PainelIndexRoute: typeof PainelIndexRoute +} + +const PainelRouteChildren: PainelRouteChildren = { + PainelConfiguracoesRoute: PainelConfiguracoesRoute, + PainelFaturamentoRoute: PainelFaturamentoRoute, + PainelIndexRoute: PainelIndexRoute, +} + +const PainelRouteWithChildren = + PainelRoute._addFileChildren(PainelRouteChildren) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginRoute: LoginRoute, + PainelRoute: PainelRouteWithChildren, + RecuperarSenhaRoute: RecuperarSenhaRoute, + RedefinirSenhaRoute: RedefinirSenhaRoute, SignupRoute: SignupRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/src/router.tsx b/src/router.tsx index adcf1a9..bde43d8 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,4 +1,5 @@ import { createRouter, useRouter } from "@tanstack/react-router"; +import { QueryClient } from "@tanstack/react-query"; import { routeTree } from "./routeTree.gen"; function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => void }) { @@ -23,9 +24,9 @@ function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => vo />
-

Something went wrong

+

Algo deu errado

- An unexpected error occurred. Please try again. + Ocorreu um erro inesperado. Tente novamente.

{import.meta.env.DEV && error.message && (
@@ -38,15 +39,15 @@ function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => vo
               router.invalidate();
               reset();
             }}
-            className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
+            className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary-dark"
           >
-            Try again
+            Tentar novamente
           
           
-            Go home
+            Ir para o início
           
         
       
@@ -55,9 +56,20 @@ function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => vo
 }
 
 export const getRouter = () => {
+  // Fresh QueryClient per request — never module-level (vaza dados entre SSR requests)
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        staleTime: 30_000,
+        retry: 1,
+        refetchOnWindowFocus: false,
+      },
+    },
+  });
+
   const router = createRouter({
     routeTree,
-    context: {},
+    context: { queryClient },
     scrollRestoration: true,
     defaultPreloadStaleTime: 0,
     defaultErrorComponent: DefaultErrorComponent,
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index ad8467f..3605010 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,9 +1,15 @@
-import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
+import { Outlet, createRootRouteWithContext, HeadContent, Scripts, Link } from "@tanstack/react-router";
+import type { QueryClient } from "@tanstack/react-query";
+import { QueryClientProvider } from "@tanstack/react-query";
 import { Toaster } from "@/components/ui/sonner";
 import { ThemeProvider } from "@/components/theme-provider";
 
 import appCss from "../styles.css?url";
 
+interface RouterContext {
+  queryClient: QueryClient;
+}
+
 function NotFoundComponent() {
   return (
     
@@ -14,19 +20,19 @@ function NotFoundComponent() { A página que você procura não existe ou foi movida.

); } -export const Route = createRootRoute({ +export const Route = createRootRouteWithContext()({ head: () => ({ meta: [ { charSet: "utf-8" }, @@ -88,10 +94,13 @@ function RootShell({ children }: { children: React.ReactNode }) { } function RootComponent() { + const { queryClient } = Route.useRouteContext(); return ( - - - - + + + + + + ); } diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 8f48862..269ffd6 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,21 +1,106 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import { Logo } from "@/components/mika/Logo"; +"use client"; + +import { useState } from "react"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { translateAuthError } from "@/lib/auth-errors"; +import { AuthCard } from "@/components/mika/AuthCard"; +import { GoogleButton } from "@/components/mika/GoogleButton"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +type LoginSearch = { redirect?: string }; + +const schema = z.object({ + email: z.string().email("Formato de e-mail inválido."), + password: z.string().min(1, "Informe sua senha."), +}); +type FormValues = z.infer; export const Route = createFileRoute("/login")({ - component: LoginPlaceholder, + validateSearch: (search: Record): LoginSearch => ({ + redirect: typeof search.redirect === "string" ? search.redirect : undefined, + }), + component: LoginPage, }); -function LoginPlaceholder() { +function LoginPage() { + const { redirect } = Route.useSearch(); + const navigate = useNavigate(); + const [submitting, setSubmitting] = useState(false); + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + }); + + const onSubmit = async (data: FormValues) => { + setSubmitting(true); + const { error } = await supabase.auth.signInWithPassword({ + email: data.email, + password: data.password, + }); + setSubmitting(false); + if (error) { + toast.error(translateAuthError(error.message)); + return; + } + toast.success("Bem-vindo de volta!"); + navigate({ to: (redirect as "/painel") || "/painel" }); + }; + return ( -
-
- -

Login

-

- A página de login completa será implementada na Etapa 2 (Auth + Supabase). -

- ← Voltar para o início + + Não tem conta?{" "} + + Criar conta + + + } + > +
+
+ + + {errors.email &&

{errors.email.message}

} +
+ +
+
+ + + Esqueci minha senha + +
+ + {errors.password &&

{errors.password.message}

} +
+ + +
+ +
+
+ ou +
-
+ + + ); } diff --git a/src/routes/painel.configuracoes.tsx b/src/routes/painel.configuracoes.tsx new file mode 100644 index 0000000..9be147e --- /dev/null +++ b/src/routes/painel.configuracoes.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { createFileRoute } from "@tanstack/react-router"; +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { IMaskInput } from "react-imask"; +import { toast } from "sonner"; +import { Loader2, LogOut } from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/hooks/use-auth"; +import { useProfile } from "@/hooks/use-profile"; +import { translateAuthError } from "@/lib/auth-errors"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; + +export const Route = createFileRoute("/painel/configuracoes")({ + component: SettingsPage, +}); + +const profileSchema = z.object({ + full_name: z.string().min(2, "Informe seu nome completo.").max(120), + company_name: z.string().max(120).optional().or(z.literal("")), + cpf_cnpj: z.string().optional().or(z.literal("")), + phone: z.string().optional().or(z.literal("")), +}); +type ProfileForm = z.infer; + +function SettingsPage() { + const { user } = useAuth(); + const { data: profile, isLoading } = useProfile(); + const queryClient = useQueryClient(); + + const { register, handleSubmit, setValue, watch, reset, formState: { errors } } = useForm({ + resolver: zodResolver(profileSchema), + }); + + useEffect(() => { + if (profile) { + reset({ + full_name: profile.full_name, + company_name: profile.company_name ?? "", + cpf_cnpj: profile.cpf_cnpj ?? "", + phone: profile.phone ?? "", + }); + } + }, [profile, reset]); + + const cpfCnpj = watch("cpf_cnpj") || ""; + const phone = watch("phone") || ""; + const cpfCnpjDigits = cpfCnpj.replace(/\D/g, ""); + const cpfCnpjMask = cpfCnpjDigits.length > 11 ? "00.000.000/0000-00" : "000.000.000-00"; + + const updateProfile = useMutation({ + mutationFn: async (data: ProfileForm) => { + if (!user) throw new Error("Sem sessão"); + const { error } = await supabase + .from("profiles") + .update({ + full_name: data.full_name, + company_name: data.company_name || null, + cpf_cnpj: data.cpf_cnpj || null, + phone: data.phone || null, + }) + .eq("id", user.id); + if (error) throw error; + }, + onSuccess: () => { + toast.success("Perfil atualizado com sucesso."); + queryClient.invalidateQueries({ queryKey: ["profile"] }); + }, + onError: (e: Error) => toast.error(translateAuthError(e.message)), + }); + + const signOutAll = async () => { + const { error } = await supabase.auth.signOut({ scope: "global" }); + if (error) toast.error(translateAuthError(error.message)); + else toast.success("Saiu de todos os dispositivos."); + }; + + if (isLoading) return ; + + return ( +
+
+

Configurações

+

Gerencie seu perfil e segurança.

+
+ +
+

Perfil

+

Informações para faturamento e suporte.

+ +
updateProfile.mutate(d))} className="space-y-4"> +
+ + + {errors.full_name &&

{errors.full_name.message}

} +
+ +
+ + +
+ +
+
+ + setValue("cpf_cnpj", v as string, { shouldValidate: true })} + placeholder="000.000.000-00" + className="flex h-10 w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20" + /> +
+ +
+ + setValue("phone", v as string, { shouldValidate: true })} + placeholder="(11) 99999-9999" + className="flex h-10 w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20" + /> +
+
+ + +
+
+ +
+

Segurança

+

+ Para alterar sua senha, use o link de recuperação no e-mail. +

+ +
+
+ ); +} diff --git a/src/routes/painel.faturamento.tsx b/src/routes/painel.faturamento.tsx new file mode 100644 index 0000000..331d81d --- /dev/null +++ b/src/routes/painel.faturamento.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { createFileRoute } from "@tanstack/react-router"; +import { format } from "date-fns"; +import { ptBR } from "date-fns/locale"; +import { CreditCard, ExternalLink } from "lucide-react"; +import { useSubscription, useProfile } from "@/hooks/use-profile"; +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; + +export const Route = createFileRoute("/painel/faturamento")({ + component: BillingPage, +}); + +function BillingPage() { + const { data: subscription, isLoading } = useSubscription(); + const { data: profile } = useProfile(); + + const { data: plan } = useQuery({ + queryKey: ["plan", subscription?.plan_id], + enabled: !!subscription?.plan_id, + queryFn: async () => { + const { data } = await supabase + .from("plans") + .select("*") + .eq("id", subscription!.plan_id!) + .maybeSingle(); + return data; + }, + }); + + return ( +
+
+

Faturamento

+

+ Gerencie sua assinatura, método de pagamento e histórico. +

+
+ + {isLoading ? ( + + ) : ( +
+

Plano atual

+ {subscription && plan ? ( +
+ + + + +
+ ) : ( +

+ Você ainda não possui uma assinatura ativa. +

+ )} + +
+ )} + +
+

Histórico de pagamentos

+

+ Seu histórico aparecerá aqui após a primeira cobrança. +

+
+ +
+

Método de pagamento

+
+ + Nenhum método cadastrado +
+ +
+
+ ); +} + +function Field({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function statusLabel(s: string): string { + return { + active: "Ativa", + trialing: "Em período de teste", + past_due: "Pagamento pendente", + canceled: "Cancelada", + incomplete: "Em provisionamento", + incomplete_expired: "Expirada", + unpaid: "Não paga", + }[s] || s; +} diff --git a/src/routes/painel.index.tsx b/src/routes/painel.index.tsx new file mode 100644 index 0000000..e339ea9 --- /dev/null +++ b/src/routes/painel.index.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { createFileRoute, Link } from "@tanstack/react-router"; +import { ArrowRight, CheckCircle2, Loader2, Sparkles } from "lucide-react"; +import { useSubscription } from "@/hooks/use-profile"; +import { useProfile } from "@/hooks/use-profile"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SubscriptionBanner } from "@/components/mika/SubscriptionBanner"; +import { cn } from "@/lib/utils"; + +export const Route = createFileRoute("/painel/")({ + component: DashboardPage, +}); + +function DashboardPage() { + const { data: subscription, isLoading } = useSubscription(); + const { data: profile } = useProfile(); + + if (isLoading) { + return ( +
+ + +
+ ); + } + + const firstName = (profile?.full_name || "").split(" ")[0] || "por aqui"; + + return ( +
+ + +
+

Olá, {firstName} 👋

+

+ {subscription + ? "Acompanhe abaixo o status do seu agente Mika." + : "Vamos colocar seu agente Mika no ar."} +

+
+ + {!subscription && } + + {subscription && (subscription.status === "incomplete" || subscription.status === "active") && ( + + )} +
+ ); +} + +function NoSubscriptionCard() { + return ( +
+
+ +
+

Escolha um plano para começar

+

+ Em poucos minutos seu agente Mika estará disponível no Telegram, com memória persistente e + skills personalizadas. +

+ +
+ ); +} + +function ProvisioningCard() { + const steps = [ + { label: "Provisionar container na VPS", state: "active" as const }, + { label: "Configurar modelo de IA", state: "pending" as const }, + { label: "Conectar seu Telegram", state: "pending" as const }, + { label: "Personalizar seu agente", state: "pending" as const }, + ]; + return ( +
+
+
+ +
+
+

Seu agente Mika está sendo provisionado

+

+ Você receberá um e-mail quando estiver pronto — geralmente em até 10 minutos. +

+
+
+ +
    + {steps.map((step, i) => ( +
  1. +
    +
    + {step.state === "active" ? ( + + ) : step.state === "pending" ? ( + i + 1 + ) : ( + + )} +
    +

    + {step.label} +

    +
    +
  2. + ))} +
+
+ ); +} diff --git a/src/routes/painel.tsx b/src/routes/painel.tsx new file mode 100644 index 0000000..f2df24b --- /dev/null +++ b/src/routes/painel.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { createFileRoute, Outlet, redirect, Link, useNavigate, useLocation } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { + Bot, + CreditCard, + Home, + LogOut, + Menu, + Plug, + Settings, + Sparkles, + User, +} from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/hooks/use-auth"; +import { useProfile } from "@/hooks/use-profile"; +import { Logo } from "@/components/mika/Logo"; +import { ThemeToggle } from "@/components/mika/ThemeToggle"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +export const Route = createFileRoute("/painel")({ + beforeLoad: () => { + // Auth real é validada client-side abaixo (Supabase usa localStorage). + // Mantemos beforeLoad como hook para futuro SSR auth. + }, + component: PainelLayout, +}); + +interface NavItem { + to: "/painel" | "/painel/faturamento" | "/painel/configuracoes"; + label: string; + icon: React.ComponentType<{ className?: string }>; + disabled?: boolean; +} + +interface DisabledNavItem { + to: null; + label: string; + icon: React.ComponentType<{ className?: string }>; + disabled: true; +} + +const NAV: (NavItem | DisabledNavItem)[] = [ + { to: "/painel", label: "Dashboard", icon: Home }, + { to: null, label: "Meu Agente", icon: Bot, disabled: true }, + { to: null, label: "Skills", icon: Sparkles, disabled: true }, + { to: null, label: "Integrações", icon: Plug, disabled: true }, + { to: "/painel/faturamento", label: "Faturamento", icon: CreditCard }, + { to: "/painel/configuracoes", label: "Configurações", icon: Settings }, +]; + +function PainelLayout() { + const { user, loading } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + if (!loading && !user) { + navigate({ to: "/login", search: { redirect: location.pathname } }); + } + }, [loading, user, navigate, location.pathname]); + + if (loading || !user) { + return ( +
+
+
+ ); + } + + return ( + +
+ + +
+
+
+ + + + + + + + + setMobileOpen(false)} /> + + +
+
+ + +
+ +
+
+ +
+
+
+
+
+ ); +} + +function DesktopSidebar() { + return ( + + ); +} + +function SidebarNav({ onNavigate }: { onNavigate?: () => void } = {}) { + const location = useLocation(); + return ( + + ); +} + +function UserMenu() { + const { user } = useAuth(); + const { data: profile } = useProfile(); + const navigate = useNavigate(); + + const initials = (profile?.full_name || user?.email || "U") + .split(" ") + .map((s) => s[0]) + .slice(0, 2) + .join("") + .toUpperCase(); + + const signOut = async () => { + await supabase.auth.signOut(); + navigate({ to: "/" }); + }; + + return ( +
+ + + + + + + +
{profile?.full_name || "Usuário"}
+
{user?.email}
+
+ + + + Perfil + + + + Sair + +
+
+
+ ); +} diff --git a/src/routes/recuperar-senha.tsx b/src/routes/recuperar-senha.tsx new file mode 100644 index 0000000..6acfe58 --- /dev/null +++ b/src/routes/recuperar-senha.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { translateAuthError } from "@/lib/auth-errors"; +import { AuthCard } from "@/components/mika/AuthCard"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +const schema = z.object({ + email: z.string().email("Formato de e-mail inválido."), +}); +type FormValues = z.infer; + +export const Route = createFileRoute("/recuperar-senha")({ + component: ForgotPasswordPage, +}); + +function ForgotPasswordPage() { + const [submitting, setSubmitting] = useState(false); + const [sent, setSent] = useState(false); + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + }); + + const onSubmit = async (data: FormValues) => { + setSubmitting(true); + const { error } = await supabase.auth.resetPasswordForEmail(data.email, { + redirectTo: `${window.location.origin}/redefinir-senha`, + }); + setSubmitting(false); + if (error) { + toast.error(translateAuthError(error.message)); + return; + } + setSent(true); + toast.success("Se houver uma conta com esse e-mail, enviamos um link de recuperação."); + }; + + return ( + + ← Voltar para o login + + } + > + {sent ? ( +
+

+ Se o e-mail informado existir em nossa base, você receberá o link em alguns minutos. + Verifique também a pasta de spam. +

+ +
+ ) : ( +
+
+ + + {errors.email &&

{errors.email.message}

} +
+ +
+ )} +
+ ); +} diff --git a/src/routes/redefinir-senha.tsx b/src/routes/redefinir-senha.tsx new file mode 100644 index 0000000..67c1c94 --- /dev/null +++ b/src/routes/redefinir-senha.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState } from "react"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { translateAuthError } from "@/lib/auth-errors"; +import { passwordStrength } from "@/lib/password"; +import { AuthCard } from "@/components/mika/AuthCard"; +import { PasswordStrengthMeter } from "@/components/mika/PasswordStrengthMeter"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +const schema = z + .object({ + password: z + .string() + .min(8, "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número.") + .refine((v) => /[A-Z]/.test(v), "A senha precisa de pelo menos 1 maiúscula.") + .refine((v) => /\d/.test(v), "A senha precisa de pelo menos 1 número."), + confirm: z.string(), + }) + .refine((d) => d.password === d.confirm, { + path: ["confirm"], + message: "As senhas não coincidem.", + }); +type FormValues = z.infer; + +export const Route = createFileRoute("/redefinir-senha")({ + component: ResetPasswordPage, +}); + +function ResetPasswordPage() { + const navigate = useNavigate(); + const [submitting, setSubmitting] = useState(false); + const { register, handleSubmit, watch, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + }); + const password = watch("password") || ""; + + const onSubmit = async (data: FormValues) => { + if (passwordStrength(data.password) < 2) { + toast.error("Escolha uma senha mais forte."); + return; + } + setSubmitting(true); + const { error } = await supabase.auth.updateUser({ password: data.password }); + setSubmitting(false); + if (error) { + toast.error(translateAuthError(error.message)); + return; + } + toast.success("Senha redefinida com sucesso. Faça login."); + await supabase.auth.signOut(); + navigate({ to: "/login" }); + }; + + return ( + +
+
+ + + {errors.password &&

{errors.password.message}

} + +
+ +
+ + + {errors.confirm &&

{errors.confirm.message}

} +
+ + +
+
+ ); +} diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 848ccdd..6536f40 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,38 +1,215 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import { Logo } from "@/components/mika/Logo"; +"use client"; + +import { useState } from "react"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { toast } from "sonner"; +import { Loader2, MailCheck } from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; +import { translateAuthError } from "@/lib/auth-errors"; +import { passwordStrength } from "@/lib/password"; +import { AuthCard } from "@/components/mika/AuthCard"; +import { GoogleButton } from "@/components/mika/GoogleButton"; +import { PasswordStrengthMeter } from "@/components/mika/PasswordStrengthMeter"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; type SignupSearch = { plan?: string; cycle?: "monthly" | "yearly"; }; +const schema = z.object({ + full_name: z.string().min(2, "Informe seu nome completo.").max(120), + email: z.string().email("Formato de e-mail inválido."), + password: z + .string() + .min(8, "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número.") + .refine((v) => /[A-Z]/.test(v), "A senha precisa de pelo menos 1 maiúscula.") + .refine((v) => /\d/.test(v), "A senha precisa de pelo menos 1 número."), + accept_terms: z.literal(true, { message: "Você precisa aceitar os termos." }), +}); +type FormValues = z.infer; + export const Route = createFileRoute("/signup")({ validateSearch: (search: Record): SignupSearch => ({ plan: typeof search.plan === "string" ? search.plan : undefined, cycle: search.cycle === "yearly" || search.cycle === "monthly" ? search.cycle : undefined, }), - component: SignupPlaceholder, + component: SignupPage, }); -function SignupPlaceholder() { +function SignupPage() { const { plan, cycle } = Route.useSearch(); - return ( -
-
- -

Criar conta

-

- Cadastro completo será implementado na Etapa 2. -

- {plan && ( -

- Plano selecionado: {plan} · {cycle === "yearly" ? "anual" : "mensal"} + const navigate = useNavigate(); + const [submitting, setSubmitting] = useState(false); + const [needsConfirm, setNeedsConfirm] = useState(null); + + const { + register, + handleSubmit, + setValue, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { accept_terms: false as unknown as true }, + }); + + const password = watch("password") || ""; + + const onSubmit = async (data: FormValues) => { + if (passwordStrength(data.password) < 2) { + toast.error("Escolha uma senha mais forte."); + return; + } + setSubmitting(true); + const { data: signupData, error } = await supabase.auth.signUp({ + email: data.email, + password: data.password, + options: { + data: { full_name: data.full_name }, + emailRedirectTo: `${window.location.origin}/painel`, + }, + }); + setSubmitting(false); + if (error) { + toast.error(translateAuthError(error.message)); + return; + } + // Sessão presente = e-mail já confirmado (auto-confirm desligado, então normalmente null) + if (signupData.session) { + toast.success("Conta criada!"); + if (plan && plan !== "enterprise") { + // TODO Etapa 3: chamar create-checkout-session com plan/cycle + navigate({ to: "/painel" }); + } else { + navigate({ to: "/painel" }); + } + return; + } + setNeedsConfirm(data.email); + }; + + const resend = async () => { + if (!needsConfirm) return; + const { error } = await supabase.auth.resend({ type: "signup", email: needsConfirm }); + if (error) { + toast.error(translateAuthError(error.message)); + return; + } + toast.success("E-mail reenviado. Confira sua caixa de entrada."); + }; + + if (needsConfirm) { + return ( + +

+
+ +
+

+ Clique no link do e-mail para ativar sua conta. Não esqueça de checar a pasta de spam.

- )} -
- ← Voltar para o início + + + Voltar para o login +
+ + ); + } + + return ( + + Já tem conta?{" "} + + Entrar + + + } + > +
+
+ + + {errors.full_name &&

{errors.full_name.message}

} +
+ +
+ + + {errors.email &&

{errors.email.message}

} +
+ +
+ + + {errors.password &&

{errors.password.message}

} + +
+ +
+ + setValue("accept_terms", v === true ? true : (false as unknown as true), { + shouldValidate: true, + }) + } + /> + +
+ {errors.accept_terms && ( +

{errors.accept_terms.message}

+ )} + + +
+ +
+
+ ou +
-
+ + + ); } From 09f706851e5c9f1a4f5b1a26037fd2b5755179e0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:05:58 +0000 Subject: [PATCH 7/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/EnterpriseLeadForm.tsx | 27 +- src/components/mika/PlansSection.tsx | 324 +++++++++++---------- src/routeTree.gen.ts | 9 - 3 files changed, 180 insertions(+), 180 deletions(-) diff --git a/src/components/mika/EnterpriseLeadForm.tsx b/src/components/mika/EnterpriseLeadForm.tsx index dd86d4f..3b06129 100644 --- a/src/components/mika/EnterpriseLeadForm.tsx +++ b/src/components/mika/EnterpriseLeadForm.tsx @@ -6,6 +6,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { IMaskInput } from "react-imask"; import { toast } from "sonner"; +import { supabase } from "@/integrations/supabase/client"; +import { translateAuthError } from "@/lib/auth-errors"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; @@ -37,17 +39,22 @@ export function EnterpriseLeadForm() { const onSubmit = async (data: FormValues) => { setSubmitting(true); - try { - // Etapa 2: gravar em enterprise_leads via supabase.from(...).insert(...) - // Por ora: feedback otimista para o usuário. - await new Promise((r) => setTimeout(r, 700)); - toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil."); - reset(); - } catch { - toast.error("Algo deu errado do nosso lado. Já estamos investigando."); - } finally { - setSubmitting(false); + const { error } = await supabase.from("enterprise_leads").insert({ + company_name: data.company_name, + contact_name: data.contact_name, + email: data.email, + phone: data.phone, + team_size: data.team_size, + message: data.message || null, + status: "new", + }); + setSubmitting(false); + if (error) { + toast.error(translateAuthError(error.message)); + return; } + toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil."); + reset(); }; const phone = watch("phone") || ""; diff --git a/src/components/mika/PlansSection.tsx b/src/components/mika/PlansSection.tsx index f06b2be..cedaa4c 100644 --- a/src/components/mika/PlansSection.tsx +++ b/src/components/mika/PlansSection.tsx @@ -3,91 +3,63 @@ import { useState } from "react"; import { Link } from "@tanstack/react-router"; import { Check } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogTrigger, +} from "@/components/ui/dialog"; import { EnterpriseLeadForm } from "./EnterpriseLeadForm"; import { cn } from "@/lib/utils"; -// Fallback estático — na Etapa 2 será substituído por query Supabase à tabela `plans`. -const STATIC_PLANS = [ - { - slug: "basic", - name: "Basic", - description: "Para começar a usar IA no dia a dia.", - monthly: 69.9, - yearly: 671.04, - highlighted: false, - is_enterprise: false, - features: [ - "1 agente pessoal no Telegram", - "Memória persistente básica", - "Integração Google Workspace", - "5 skills personalizadas", - "Suporte por e-mail", - ], - }, - { - slug: "starter", - name: "Starter", - description: "Para quem usa IA todos os dias.", - monthly: 199.9, - yearly: 1919.04, - highlighted: false, - is_enterprise: false, - features: [ - "Tudo do Basic", - "Memória avançada e contextual", - "Skills ilimitadas", - "Agendamentos automáticos", - "Suporte prioritário", - ], - }, - { - slug: "professional", - name: "Professional", - description: "Para profissionais e times pequenos.", - monthly: 399.9, - yearly: 3839.04, - highlighted: true, - is_enterprise: false, - features: [ - "Tudo do Starter", - "VPS dedicada de alta performance", - "Modelos de IA premium", - "Integrações personalizadas", - "Onboarding 1:1", - "Suporte em horário estendido", - ], - }, - { - slug: "enterprise", - name: "Enterprise", - description: "Para empresas com necessidades específicas.", - monthly: null, - yearly: null, - highlighted: false, - is_enterprise: true, - features: [ - "Tudo do Professional", - "Múltiplos agentes", - "SSO e gestão de equipe", - "SLA contratual", - "Treinamento da equipe", - "Account manager dedicado", - ], - }, -] as const; +interface PlanRow { + id: string; + slug: string; + name: string; + description: string | null; + price_monthly_brl: number | null; + price_yearly_brl: number | null; + features: string[]; + highlighted: boolean; + is_enterprise: boolean; + display_order: number; +} const fmtBRL = (v: number) => v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +function usePlans() { + return useQuery({ + queryKey: ["plans"], + staleTime: 5 * 60_000, + queryFn: async (): Promise => { + const { data, error } = await supabase + .from("plans") + .select("*") + .order("display_order", { ascending: true }); + if (error) throw error; + return (data || []).map((p) => ({ + ...p, + features: Array.isArray(p.features) ? (p.features as string[]) : [], + })) as PlanRow[]; + }, + }); +} + export function PlansSection() { const [yearly, setYearly] = useState(false); + const { data: plans, isLoading } = usePlans(); return ( -
+

Planos para cada estágio

@@ -96,110 +68,140 @@ export function PlansSection() {

- + Mensal - - + + Anual - 20% off + + 20% off +
- {STATIC_PLANS.map((plan) => { - const price = yearly ? plan.yearly : plan.monthly; - const monthlyEquivalent = yearly && plan.yearly ? plan.yearly / 12 : null; - return ( -
- {plan.highlighted && ( - - Mais popular - - )} + {isLoading + ? Array.from({ length: 4 }).map((_, i) => ( + + )) + : (plans || []).map((plan) => { + const monthly = plan.price_monthly_brl ?? 0; + const yearlyPrice = plan.price_yearly_brl ?? 0; + const monthlyEquivalent = yearly && yearlyPrice ? yearlyPrice / 12 : monthly; + return ( +
+ {plan.highlighted && ( + + Mais popular + + )} -
-

{plan.name}

-

{plan.description}

-
+
+

{plan.name}

+

{plan.description}

+
-
- {plan.is_enterprise ? ( -

Sob consulta

- ) : ( - <> -
- R$ - - {fmtBRL(yearly && monthlyEquivalent ? monthlyEquivalent : (price as number))} - - /mês -
- {yearly && ( -

- R$ {fmtBRL(plan.yearly!)} cobrados anualmente -

+
+ {plan.is_enterprise ? ( +

Sob consulta

+ ) : ( + <> +
+ R$ + + {fmtBRL(monthlyEquivalent)} + + /mês +
+ {yearly && yearlyPrice > 0 && ( +

+ R$ {fmtBRL(yearlyPrice)} cobrados anualmente +

+ )} + )} - - )} -
+
-
    - {plan.features.map((f) => ( -
  • - - {f} -
  • - ))} -
+
    + {plan.features.map((f) => ( +
  • + + {f} +
  • + ))} +
-
- {plan.is_enterprise ? ( - - - - - - - Plano Enterprise - - Conte um pouco sobre sua empresa. Nosso time entra em contato em até 1 dia útil. - - - - - - ) : ( - + + + + Plano Enterprise + + Conte um pouco sobre sua empresa. Nosso time entra em contato em até + 1 dia útil. + + + + + + ) : ( + )} - > - - Assinar agora - - - )} -

- Sujeito à política de uso justo. Veja termos. -

-
-
- ); - })} +

+ Sujeito à política de uso justo. Veja termos. +

+
+ + ); + })}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index dbc2b0c..8c8dbaa 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -236,12 +236,3 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } -} From f20a92047b154ab15939ee2d9e0a3be224016161 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:06:17 +0000 Subject: [PATCH 8/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/LandingHeader.tsx | 42 +++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/components/mika/LandingHeader.tsx b/src/components/mika/LandingHeader.tsx index 0c65c82..dc7cf5c 100644 --- a/src/components/mika/LandingHeader.tsx +++ b/src/components/mika/LandingHeader.tsx @@ -5,6 +5,7 @@ import { Link } from "@tanstack/react-router"; import { Menu } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetHeader } from "@/components/ui/sheet"; +import { useAuth } from "@/hooks/use-auth"; import { Logo } from "./Logo"; import { ThemeToggle } from "./ThemeToggle"; import { cn } from "@/lib/utils"; @@ -19,6 +20,7 @@ const NAV = [ export function LandingHeader() { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); + const { user } = useAuth(); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8); @@ -53,12 +55,20 @@ export function LandingHeader() {
- - + {user ? ( + + ) : ( + <> + + + + )}
@@ -86,12 +96,20 @@ export function LandingHeader() { ))}
- - + {user ? ( + + ) : ( + <> + + + + )}
From af546e95a2cb297c51c980171e82b7bbcc911e3d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:06:49 +0000 Subject: [PATCH 9/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routeTree.gen.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 8c8dbaa..dbc2b0c 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -236,3 +236,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +}