From 2e8c4fe7bc218e0ec77ae1e11b079778852e2c6e Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 17:10:47 -0300 Subject: [PATCH 01/11] feat(MobileNavbarItem): children with onClick can close menu adhoc --- src/components/Navbar/MobileNavbarItem.tsx | 11 ++++++++++- src/components/Navbar/types.d.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/Navbar/MobileNavbarItem.tsx b/src/components/Navbar/MobileNavbarItem.tsx index 6ef0428..f2bdb86 100644 --- a/src/components/Navbar/MobileNavbarItem.tsx +++ b/src/components/Navbar/MobileNavbarItem.tsx @@ -28,7 +28,16 @@ export const MobileNavbarItem = ({ return ( { + if (item.onClick) { + item.onClick(e); + } + if (item.closeMenu) { + setOpen(false); + } + }} > {item.content} diff --git a/src/components/Navbar/types.d.ts b/src/components/Navbar/types.d.ts index 87c07bd..f6a62be 100644 --- a/src/components/Navbar/types.d.ts +++ b/src/components/Navbar/types.d.ts @@ -6,6 +6,7 @@ export type NavbarMenuItem = { icon?: React.ReactNode; onClick?: (e: React.MouseEvent) => void; children?: Array; + closeMenu?: boolean; }; export type NavBarProps = { From 90f54abbc64224b60097cc967e461ac7cc0b40a4 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 15:48:41 -0300 Subject: [PATCH 02/11] feat(Nav): avoid new window for click on Logo / Home --- src/components/nav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 61f38bc..e412d92 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -79,7 +79,7 @@ export const Nav = () => { return (
- +
Devent From 294c250b98e072df9e9a60a316b825107c4dd2e7 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sat, 30 Mar 2024 15:13:51 -0300 Subject: [PATCH 03/11] chore(Auth): include Supabase needs/packages/boilerplates --- .env.local.example | 3 +- middleware.ts | 19 +++++ package-lock.json | 127 ++++++++++++++++++++++++++++++- package.json | 4 + src/utils/supabase/client.ts | 8 ++ src/utils/supabase/middleware.ts | 64 ++++++++++++++++ src/utils/supabase/server.ts | 38 +++++++++ 7 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 middleware.ts create mode 100644 src/utils/supabase/client.ts create mode 100644 src/utils/supabase/middleware.ts create mode 100644 src/utils/supabase/server.ts diff --git a/.env.local.example b/.env.local.example index f1a46f4..0867476 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,4 +1,5 @@ NEXT_PUBLIC_JSCL_API_URL='https://api.jsconf.dev/graphql' NEXT_PUBLIC_TOKEN_STORAGE_KEY='HS:token_storage_key' -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY='pk_test_ZnVua3ktZ3JpZmZvbi04NC5jbGVyay5hY2NvdW50cy5kZXYk' NEXT_PUBLIC_GOOGLE_MAPS_KEY='your_google_maps_key_here' +NEXT_PUBLIC_SUPABASE_URL='your_supabase_url' +NEXT_PUBLIC_SUPABASE_ANON_KEY='your_supabase_key' \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..01abe41 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,19 @@ +import { type NextRequest } from 'next/server' +import { updateSession } from '@/utils/supabase/middleware' + +export async function middleware(request: NextRequest) { + return await updateSession(request) +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 277e3b1..d51f5ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,10 @@ "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@supabase/auth-ui-react": "^0.4.7", + "@supabase/auth-ui-shared": "^0.1.8", + "@supabase/ssr": "^0.1.0", + "@supabase/supabase-js": "^2.41.1", "@types/node": "20.4.1", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", @@ -5000,6 +5004,113 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@stitches/core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", + "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==" + }, + "node_modules/@supabase/auth-js": { + "version": "2.63.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.63.0.tgz", + "integrity": "sha512-yIgcHnlgv24GxHtVGUhwGqAFDyJkPIC/xjx7HostN08A8yCy8HIfl4JEkTKyBqD1v1L05jNEJOUke4Lf4O1+Qg==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/auth-ui-react": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-react/-/auth-ui-react-0.4.7.tgz", + "integrity": "sha512-Lp4FQGFh7BMX1Y/BFaUKidbryL7eskj1fl6Lby7BeHrTctbdvDbCMjVKS8wZ2rxuI8FtPS2iU900fSb70FHknQ==", + "dependencies": { + "@stitches/core": "^1.2.8", + "@supabase/auth-ui-shared": "0.1.8", + "prop-types": "^15.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.21.0" + } + }, + "node_modules/@supabase/auth-ui-shared": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-shared/-/auth-ui-shared-0.1.8.tgz", + "integrity": "sha512-ouQ0DjKcEFg+0gZigFIEgu01V3e6riGZPzgVD0MJsCBNsMsiDT74+GgCEIElMUpTGkwSja3xLwdFRFgMNFKcjg==", + "peerDependencies": { + "@supabase/supabase-js": "^2.21.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.2.2.tgz", + "integrity": "sha512-sJGq1nludmi7pY/fdtCpyY/pYonx7MfHdN408bqb736guWcVI1AChYVbI4kUM978EuOE4Ci6l7bUudfGg07QRw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.9.2.tgz", + "integrity": "sha512-I6yHo8CC9cxhOo6DouDMy9uOfW7hjdsnCxZiaJuIVZm1dBGTFiQPgfMa9zXCamEWzNyWRjZvupAUuX+tqcl5Sw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.3.tgz", + "integrity": "sha512-lAp50s2n3FhGJFq+wTSXLNIDPw5Y0Wxrgt44eM5nLSA3jZNUUP3Oq2Ccd1CbZdVntPCWLZvJaU//pAd2NE+QnQ==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.1.0.tgz", + "integrity": "sha512-bIVrkqjAK5G3KjkIMKYKtAOlCgRRplEWjrlyRyXSOYtgDieiOhk2ZyNAPsEOa1By9OZVxuX5eAW1fitdnuxayw==", + "dependencies": { + "cookie": "^0.5.0", + "ramda": "^0.29.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.33.1" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz", + "integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.41.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.41.1.tgz", + "integrity": "sha512-xmECLhYugMo/6SObpsOhu5xaxVfsk+JK/d4JSh055bpESmgmN3PLpzbfqejKCpaUeeUNF21+lrJp/U9HQzT9mA==", + "dependencies": { + "@supabase/auth-js": "2.63.0", + "@supabase/functions-js": "2.2.2", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.9.2", + "@supabase/realtime-js": "2.9.3", + "@supabase/storage-js": "2.5.5" + } + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -5177,6 +5288,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==" }, + "node_modules/@types/phoenix": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz", + "integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -5234,7 +5350,6 @@ "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -11617,6 +11732,15 @@ } ] }, + "node_modules/ramda": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz", + "integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -13560,7 +13684,6 @@ "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 5473f68..e523056 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,10 @@ "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@supabase/auth-ui-react": "^0.4.7", + "@supabase/auth-ui-shared": "^0.1.8", + "@supabase/ssr": "^0.1.0", + "@supabase/supabase-js": "^2.41.1", "@types/node": "20.4.1", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", diff --git a/src/utils/supabase/client.ts b/src/utils/supabase/client.ts new file mode 100644 index 0000000..2ad2591 --- /dev/null +++ b/src/utils/supabase/client.ts @@ -0,0 +1,8 @@ +import { createBrowserClient } from "@supabase/ssr"; + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + ); +} diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts new file mode 100644 index 0000000..c2da0bb --- /dev/null +++ b/src/utils/supabase/middleware.ts @@ -0,0 +1,64 @@ +import { createServerClient, type CookieOptions } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +export async function updateSession(request: NextRequest) { + let response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + get(name: string) { + return request.cookies.get(name)?.value; + }, + set(name: string, value: string, options: CookieOptions) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + request.cookies.set({ + name, + value, + ...options, + }); + response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + response.cookies.set({ + name, + value, + ...options, + }); + }, + remove(name: string, options: CookieOptions) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + request.cookies.set({ + name, + value: "", + ...options, + }); + response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + response.cookies.set({ + name, + value: "", + ...options, + }); + }, + }, + }, + ); + + await supabase.auth.getUser(); + + return response; +} diff --git a/src/utils/supabase/server.ts b/src/utils/supabase/server.ts new file mode 100644 index 0000000..8a186d0 --- /dev/null +++ b/src/utils/supabase/server.ts @@ -0,0 +1,38 @@ +import { createServerClient, type CookieOptions } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export function createClient() { + const cookieStore = cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + get(name: string) { + return cookieStore.get(name)?.value; + }, + set(name: string, value: string, options: CookieOptions) { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + cookieStore.set({ name, value, ...options }); + } catch (error) { + // The `set` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + remove(name: string, options: CookieOptions) { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + cookieStore.set({ name, value: "", ...options }); + } catch (error) { + // The `delete` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }, + ); +} From 22d85a860614a7569047d37ee0b19f11dd7aefc7 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 15:23:01 -0300 Subject: [PATCH 04/11] feat(Login): define Page with Supabase auth --- app/(transition)/(root)/login/page.tsx | 91 ++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/(transition)/(root)/login/page.tsx diff --git a/app/(transition)/(root)/login/page.tsx b/app/(transition)/(root)/login/page.tsx new file mode 100644 index 0000000..48726b5 --- /dev/null +++ b/app/(transition)/(root)/login/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { NextPage } from "next"; + +import { redirect } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Session } from "@supabase/supabase-js"; +import { Auth } from "@supabase/auth-ui-react"; +import { ThemeSupa } from "@supabase/auth-ui-shared"; +import { createClient } from "@/utils/supabase/client"; +import { useTheme } from "next-themes"; + +const supabase = createClient(); + +const AuthPage: NextPage = () => { + const [session, setSession] = useState(null); + const { resolvedTheme } = useTheme(); + const [url, setUrl] = useState(); + + useEffect(() => { + const url = new URL(window.location.href); + setUrl(url.toString()); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + supabase.auth.getSession().then(async ({ data: { session } }) => { + setSession(session); + await supabase.auth.startAutoRefresh(); + }); + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(async (_event, session) => { + await supabase.auth.startAutoRefresh(); + setSession(session); + }); + + return () => subscription.unsubscribe(); + }, []); + + if (session) { + redirect("/"); + } + + return ( +
+
+
+

Regístrate.

+

+ Mantente al tanto de las novedades de JavaScript Chile. +

+
+ + +
+
+ ); +}; + +export default AuthPage; From 074ca0b77346fc47b5c9d5981f48090304d1c343 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 15:23:27 -0300 Subject: [PATCH 05/11] feat(MainNav): move from Clerk to Supabase --- src/components/Navbar/MainNav.tsx | 80 ++++++++++++++++++------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/components/Navbar/MainNav.tsx b/src/components/Navbar/MainNav.tsx index cefc9ce..301a375 100644 --- a/src/components/Navbar/MainNav.tsx +++ b/src/components/Navbar/MainNav.tsx @@ -1,48 +1,62 @@ +import Link from "next/link"; +import { useRouter } from "next/navigation"; + import { NavBarProps } from "./types"; import { NavbarItem } from "./NavbarItem"; -import { - SignInButton, - SignOutButton, - SignedIn, - SignedOut, -} from "@clerk/clerk-react"; -import { Button } from "../ui/button"; -import { usePathname } from "next/navigation"; +import { Button, buttonVariants } from "../ui/button"; import { useEffect, useState } from "react"; +import { User } from "@supabase/supabase-js"; +import { createClient } from "@/utils/supabase/client"; export function MainNav({ items }: NavBarProps) { - const pathname = usePathname(); - const [redirectUrl, setRedirectUrl] = useState(""); + const supabase = createClient(); + const router = useRouter(); + const [user, setUser] = useState(null); + useEffect(() => { - setRedirectUrl(window.location.host); + const getUser = async () => { + const { data } = await supabase.auth.getSession(); + setUser(data?.session?.user ?? null); + }; + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + }); + + return () => subscription.unsubscribe(); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getUser(); }, []); + + const handleLogout = () => { + const signOut = async () => { + const { error } = await supabase.auth.signOut(); + if (!error) { + router.push("/login"); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + signOut(); + }; + return ( ); } From ee5e723ccbb107a68176ccfcd9ed60e000fcd368 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 15:47:45 -0300 Subject: [PATCH 06/11] feat(Nav): move from Clerk to Supabase Nav, MainNavbar & MobileNavbar --- src/components/Navbar/MainNav.tsx | 30 +++-------- src/components/Navbar/MobileNav.tsx | 82 +++++++++++++---------------- src/components/nav.tsx | 53 +++++++++++++++---- 3 files changed, 88 insertions(+), 77 deletions(-) diff --git a/src/components/Navbar/MainNav.tsx b/src/components/Navbar/MainNav.tsx index 301a375..7acfafb 100644 --- a/src/components/Navbar/MainNav.tsx +++ b/src/components/Navbar/MainNav.tsx @@ -1,21 +1,21 @@ import Link from "next/link"; -import { useRouter } from "next/navigation"; import { NavBarProps } from "./types"; import { NavbarItem } from "./NavbarItem"; -import { Button, buttonVariants } from "../ui/button"; +import { buttonVariants } from "../ui/button"; import { useEffect, useState } from "react"; import { User } from "@supabase/supabase-js"; import { createClient } from "@/utils/supabase/client"; export function MainNav({ items }: NavBarProps) { const supabase = createClient(); - const router = useRouter(); const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { const getUser = async () => { const { data } = await supabase.auth.getSession(); + setLoading(false); setUser(data?.session?.user ?? null); }; @@ -25,38 +25,22 @@ export function MainNav({ items }: NavBarProps) { setUser(session?.user ?? null); }); - return () => subscription.unsubscribe(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises getUser(); - }, []); - - const handleLogout = () => { - const signOut = async () => { - const { error } = await supabase.auth.signOut(); - if (!error) { - router.push("/login"); - } - }; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - signOut(); - }; + return () => subscription.unsubscribe(); + }, []); return ( ); } diff --git a/src/components/Navbar/MobileNav.tsx b/src/components/Navbar/MobileNav.tsx index 37aace6..fab2eb5 100644 --- a/src/components/Navbar/MobileNav.tsx +++ b/src/components/Navbar/MobileNav.tsx @@ -1,29 +1,41 @@ "use client"; -import * as React from "react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Menu, PackageOpen } from "lucide-react"; +import { User } from "@supabase/supabase-js"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { NavBarProps } from "./types"; import { MobileNavbarItem } from "./MobileNavbarItem"; import { MobileLink } from "./MobileLink"; -import { Menu, PackageOpen } from "lucide-react"; -import { - SignInButton, - SignOutButton, - SignedIn, - SignedOut, -} from "@clerk/clerk-react"; -import { usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; +import { createClient } from "@/utils/supabase/client"; export function MobileNav({ items }: NavBarProps) { const [open, setOpen] = useState(false); - const pathname = usePathname(); - const [redirectUrl, setRedirectUrl] = useState(""); + const supabase = createClient(); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + useEffect(() => { - setRedirectUrl(window.location.host); + const getUser = async () => { + const { data } = await supabase.auth.getSession(); + setUser(data?.session?.user ?? null); + setLoading(false); + }; + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + }); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getUser(); + + return () => subscription.unsubscribe(); }, []); return ( @@ -55,37 +67,17 @@ export function MobileNav({ items }: NavBarProps) { setOpen={setOpen} /> ))} - - {process.env.NEXT_PUBLIC_SIGN_IN_URL ? ( - - ) : ( - - - - )} - - - -
{ - setOpen(false); - }} - // item={{ link: "#", content: "Salir" }} - // setOpen={setOpen} - > - Salir -
-
-
+ {!loading && !user ? ( + { + setOpen(false); + }} + > + Ingresar + + ) : null}
diff --git a/src/components/nav.tsx b/src/components/nav.tsx index e412d92..6be392b 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -1,18 +1,52 @@ "use client"; import Link from "next/link"; +import { useMemo, useEffect, useState } from "react"; +import { LogOut, Settings, User as UserIcon, PackageOpen } from "lucide-react"; +import { User } from "@supabase/supabase-js"; +import { useRouter } from "next/navigation"; + +import { createClient } from "@/utils/supabase/client"; + import { MainNav } from "./Navbar/MainNav"; import { MobileNav } from "./Navbar/MobileNav"; import { ThemeSwitcher } from "./Navbar/ThemeSwitcher"; -import { LogOut, Settings, User, PackageOpen } from "lucide-react"; -import { useClerk, useUser } from "@clerk/clerk-react"; -import { useMemo } from "react"; import { NavbarMenuItem } from "./Navbar/types"; export const Nav = () => { - const { isLoaded, isSignedIn } = useUser(); - const isLogged = useMemo(() => isLoaded && isSignedIn, []); - const { signOut } = useClerk(); + const supabase = createClient(); + const router = useRouter(); + const [user, setUser] = useState(null); + const isLogged = !!user?.id; + + useEffect(() => { + const getUser = async () => { + const { data } = await supabase.auth.getSession(); + setUser(data?.session?.user ?? null); + }; + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + }); + + return () => subscription.unsubscribe(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getUser(); + }, []); + + const handleLogout = () => { + const signOut = async () => { + const { error } = await supabase.auth.signOut(); + if (!error) { + router.push("/login"); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + signOut(); + }; const guestItems = useMemo( () => @@ -45,7 +79,7 @@ export const Nav = () => { children: [ { content: "Mi Cuenta", - icon: , + icon: , link: "/", }, { @@ -67,13 +101,14 @@ export const Nav = () => { if (key) { window.localStorage.clear(); } - signOut().catch((e) => console.error(e)); + handleLogout(); }, + closeMenu: true, }, ], }, ] satisfies NavbarMenuItem[], - [signOut], + [handleLogout], ); return ( From 4632bd52d7aa493630f86e89543c5e7dcb0f82b8 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 18:23:43 -0300 Subject: [PATCH 07/11] chore(Auth): remove orphan Auth logic --- src/components/Event/Register/Register.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Event/Register/Register.tsx b/src/components/Event/Register/Register.tsx index 94f2ce1..57dfbd8 100644 --- a/src/components/Event/Register/Register.tsx +++ b/src/components/Event/Register/Register.tsx @@ -2,11 +2,8 @@ import { Button } from "@/components/ui/button"; import { InformationCircleIcon } from "@heroicons/react/24/outline"; -import { useAuth } from "@clerk/clerk-react"; export const Register = () => { - const { isSignedIn } = useAuth(); - return (

@@ -27,7 +24,7 @@ export const Register = () => {

Para registrarte, por favor haz click en el botón.

-

From ba66952c137238f06575a0c8e8fe8255565d8447 Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Sun, 31 Mar 2024 18:24:12 -0300 Subject: [PATCH 08/11] chore(Auth): remove Clerk --- app/(transition)/layout.tsx | 28 ++++++++----- app/layout.tsx | 36 +++++++---------- package-lock.json | 78 ------------------------------------- package.json | 1 - 4 files changed, 33 insertions(+), 110 deletions(-) diff --git a/app/(transition)/layout.tsx b/app/(transition)/layout.tsx index d23c104..4935a3f 100644 --- a/app/(transition)/layout.tsx +++ b/app/(transition)/layout.tsx @@ -1,30 +1,40 @@ "use client"; -import { useAuth } from "@clerk/clerk-react"; import { AnimatePresence, LazyMotion, domAnimation } from "framer-motion"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; + +import { createClient } from "@/utils/supabase/client"; export default function Layout({ children }: { children: React.ReactNode }) { - const { getToken } = useAuth(); + const supabase = createClient(); useEffect(() => { const start = async () => { - const token = await getToken({ - template: "API_AUTH", - }); + const { data } = await supabase.auth.getSession(); + const accessToken = data?.session?.access_token; + const localStorageKey = process.env.NEXT_PUBLIC_TOKEN_STORAGE_KEY; - if (!token || !localStorageKey) { + if (!accessToken || !localStorageKey) { return; } + window.localStorage.setItem( process.env.NEXT_PUBLIC_TOKEN_STORAGE_KEY!, - token, + accessToken, ); - console.log({ token }); + console.log({ accessToken }); }; + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => {}); + start().catch((e) => { console.error(e); }); + + return () => subscription.unsubscribe(); }, []); + return ( {children} diff --git a/app/layout.tsx b/app/layout.tsx index fae2255..e430807 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,7 +3,6 @@ import { Inter, Roboto } from "next/font/google"; import classNames from "classnames"; import { ThemeProvider } from "@/components/providers"; import { ApolloWrapper } from "../src/api/ApolloWrapper"; -import { Clerk } from "../src/components/Auth/clerk"; const inter = Inter({ subsets: ["latin"], @@ -29,27 +28,20 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - - - {children} - - - - - + + + + + {children} + + + + ); } diff --git a/package-lock.json b/package-lock.json index d51f5ba..0da9361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@apollo/client": "^3.9.0-alpha.4", "@apollo/experimental-nextjs-app-support": "^0.5.1", "@auth0/nextjs-auth0": "^2.6.3", - "@clerk/clerk-react": "^4.28.0", "@heroicons/react": "^2.0.18", "@next/third-parties": "^14.0.4", "@radix-ui/react-dialog": "^1.0.4", @@ -1296,56 +1295,6 @@ "node": ">=6.9.0" } }, - "node_modules/@clerk/clerk-react": { - "version": "4.28.3", - "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-4.28.3.tgz", - "integrity": "sha512-keiQSpl9EVP7XNKOLXRs+zxkrGG8FRueMdn0+GcOUlmfIUcE9HFhCX/2wCt7OiLuuZLJpMOVEg9weDepW5APEg==", - "dependencies": { - "@clerk/shared": "1.1.1", - "@clerk/types": "3.58.1", - "tslib": "2.4.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": ">=16" - } - }, - "node_modules/@clerk/clerk-react/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/@clerk/shared": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-1.1.1.tgz", - "integrity": "sha512-pEzhalD1Yo/gGsOE2BQugVQTjlIl2aYmoeRld3BDXHRDV1jnk+yUE2CFOw6bojgFWN9sbeN/ph/47UWvvoCSOg==", - "dependencies": { - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.1", - "swr": "2.2.0" - }, - "peerDependencies": { - "react": ">=16" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, - "node_modules/@clerk/types": { - "version": "3.58.1", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-3.58.1.tgz", - "integrity": "sha512-cumryWXAYNCApsc3pmuHiJQnhUcNuQPCMI8w/bVU9VuGm9Ry0zhCWawD7F3/VWAOnAPu4ghKrwEaLkGsiSsY2Q==", - "dependencies": { - "csstype": "3.1.1" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/@codemirror/language": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", @@ -9392,14 +9341,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", - "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==", - "engines": { - "node": ">=12" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12752,17 +12693,6 @@ "tslib": "^2.0.3" } }, - "node_modules/swr": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", - "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", - "dependencies": { - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -13446,14 +13376,6 @@ } } }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index e523056..8e03e12 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@apollo/client": "^3.9.0-alpha.4", "@apollo/experimental-nextjs-app-support": "^0.5.1", "@auth0/nextjs-auth0": "^2.6.3", - "@clerk/clerk-react": "^4.28.0", "@heroicons/react": "^2.0.18", "@next/third-parties": "^14.0.4", "@radix-ui/react-dialog": "^1.0.4", From 6d9aab1119fec4482c7bf887207b8852aa4c08aa Mon Sep 17 00:00:00 2001 From: Jose Lezama Date: Mon, 1 Apr 2024 00:25:45 -0300 Subject: [PATCH 09/11] chore(GQL_API): auto update --- src/api/gql/graphql.ts | 209 ++++++++++++++++++++++++++++++++++++----- src/api/gql/schema.gql | 193 ++++++++++++++++++++++++++++++++----- 2 files changed, 356 insertions(+), 46 deletions(-) diff --git a/src/api/gql/graphql.ts b/src/api/gql/graphql.ts index 4f18429..6b48f6f 100644 --- a/src/api/gql/graphql.ts +++ b/src/api/gql/graphql.ts @@ -28,6 +28,7 @@ export type Scalars = { export type AllowedCurrency = { currency: Scalars['String']['output']; id: Scalars['String']['output']; + validPaymentMethods: ValidPaymentMethods; }; export enum CommnunityStatus { @@ -65,6 +66,14 @@ export enum CompanyStatus { Inactive = 'inactive' } +/** Representation of a consolidated payment entry log calculation */ +export type ConsolidatedPaymentLogEntry = { + currencyId: Scalars['String']['output']; + id: Scalars['String']['output']; + platform: Scalars['String']['output']; + totalTransactionAmount: Scalars['Float']['output']; +}; + export type CreateCommunityInput = { description: Scalars['String']['input']; name: Scalars['String']['input']; @@ -86,18 +95,24 @@ export type CreateSalaryInput = { companyId: Scalars['String']['input']; confirmationToken: Scalars['String']['input']; countryCode: Scalars['String']['input']; - currencyId: Scalars['String']['input']; + currencyCode: Scalars['String']['input']; gender: Gender; genderOtherText: Scalars['String']['input']; typeOfEmployment: TypeOfEmployment; workMetodology: WorkMetodology; - workRoleId: Scalars['String']['input']; + workSeniorityAndRoleId: Scalars['String']['input']; yearsOfExperience: Scalars['Int']['input']; }; +export enum EmailStatus { + Confirmed = 'confirmed', + Pending = 'pending', + Rejected = 'rejected' +} + export type EnqueueGoogleAlbumImportInput = { albumId: Scalars['String']['input']; - sanityEventInstanceId: Scalars['String']['input']; + sanityEventId: Scalars['String']['input']; token: Scalars['String']['input']; }; @@ -108,6 +123,7 @@ export type Event = { description: Maybe; endDateTime: Maybe; id: Scalars['String']['output']; + images: Array; latitude: Maybe; longitude: Maybe; maxAttendees: Maybe; @@ -116,14 +132,17 @@ export type Event = { startDateTime: Scalars['DateTime']['output']; status: EventStatus; tags: Array; - tickets: Array; + /** List of tickets for sale or redemption for this event. (If you are looking for a user's tickets, use the usersTickets field) */ + tickets: Array; users: Array; + /** List of tickets that a user owns for this event. */ + usersTickets: Array; visibility: EventVisibility; }; /** Representation of an Event (Events and Users, is what tickets are linked to) */ -export type EventTicketsArgs = { +export type EventUsersTicketsArgs = { input: InputMaybe; }; @@ -159,6 +178,11 @@ export type EventEditInput = { visibility: InputMaybe; }; +/** Search for tags */ +export type EventImageSearch = { + eventId: Scalars['String']['input']; +}; + export enum EventStatus { Active = 'active', Inactive = 'inactive' @@ -206,6 +230,8 @@ export type Mutation = { approvalUserTicket: UserTicket; /** Cancel a ticket */ cancelUserTicket: UserTicket; + /** Attempt to claim a certain ammount of tickets */ + claimUserTicket: RedeemUserTicketResponse; /** Create an community */ createCommunity: Community; /** Create a company */ @@ -214,6 +240,8 @@ export type Mutation = { createEvent: Event; /** Create a salary */ createSalary: Salary; + /** Create a ticket */ + createTicket: Ticket; /** Edit an community */ editCommunity: Community; /** Edit an event */ @@ -249,6 +277,11 @@ export type MutationCancelUserTicketArgs = { }; +export type MutationClaimUserTicketArgs = { + input: TicketClaimInput; +}; + + export type MutationCreateCommunityArgs = { input: CreateCommunityInput; }; @@ -269,6 +302,11 @@ export type MutationCreateSalaryArgs = { }; +export type MutationCreateTicketArgs = { + input: TicketCreateInput; +}; + + export type MutationEditCommunityArgs = { input: UpdateCommunityInput; }; @@ -331,6 +369,35 @@ export type MyTicketsSearchInput = { status: InputMaybe; }; +/** Representation of a TicketPrice */ +export type Price = { + amount: Scalars['Int']['output']; + currency: AllowedCurrency; + id: Scalars['ID']['output']; +}; + +/** Representation of a payment log entry */ +export type PublicFinanceEntryRef = { + createdAt: Scalars['DateTime']['output']; + currencyId: Scalars['String']['output']; + id: Scalars['String']['output']; + platform: Scalars['String']['output']; + transactionAmount: Scalars['Float']['output']; + transactionDate: Maybe; +}; + +/** Representation of a Purchase Order */ +export type PurchaseOrder = { + id: Scalars['ID']['output']; + tickets: Array; + totalAmount: Maybe; +}; + +export type PurchaseOrderInput = { + quantity: Scalars['Int']['input']; + ticketId: Scalars['String']['input']; +}; + export type Query = { /** Get a list of communities. Filter by name, id, or status */ communities: Array; @@ -342,12 +409,20 @@ export type Query = { company: Company; /** Get an event by id */ event: Maybe; + /** Get a list of images, that are attached to an event */ + eventImages: Array; /** Get a list of events. Filter by name, id, status or date */ events: Array; /** Get the current user */ me: User; /** Get a list of tickets for the current user */ myTickets: Array; + /** Get a list of salaries associated to the user */ + salaries: Array; + /** Search a consolidated payment logs, by date, aggregated by platform and currency_id */ + searchConsolidatedPaymentLogs: Array; + /** Search on the payment logs by date, and returns a list of payment logs */ + searchPaymentLogs: Array; status: Scalars['String']['output']; /** Get a list of tags */ tags: Array; @@ -357,6 +432,12 @@ export type Query = { users: Array; /** Get a workEmail and check if its validated for this user */ workEmail: WorkEmail; + /** Get a list of validated work emails for the user */ + workEmails: Array; + /** Get a a work role's seniorities */ + workRoleSeniorities: Array; + /** Get a list of possible work roles */ + workRoles: Array; }; @@ -387,6 +468,11 @@ export type QueryEventArgs = { }; +export type QueryEventImagesArgs = { + input: EventImageSearch; +}; + + export type QueryEventsArgs = { input: InputMaybe; }; @@ -397,6 +483,16 @@ export type QueryMyTicketsArgs = { }; +export type QuerySearchConsolidatedPaymentLogsArgs = { + input: SearchPaymentLogsInput; +}; + + +export type QuerySearchPaymentLogsArgs = { + input: SearchPaymentLogsInput; +}; + + export type QueryStatusArgs = { name: InputMaybe; }; @@ -416,18 +512,31 @@ export type QueryWorkEmailArgs = { email: Scalars['String']['input']; }; + +export type QueryWorkRoleSenioritiesArgs = { + input: WorkRoleSenioritiesInput; +}; + +export type RedeemUserTicketError = { + error: Scalars['Boolean']['output']; + errorMessage: Scalars['String']['output']; +}; + +export type RedeemUserTicketResponse = PurchaseOrder | RedeemUserTicketError; + /** Representation of a workEmail */ export type Salary = { amount: Scalars['Int']['output']; company: Company; countryCode: Scalars['String']['output']; - currency: AllowedCurrency; + currencyCode: Scalars['String']['output']; gender: Maybe; genderOtherText: Maybe; id: Scalars['String']['output']; typeOfEmployment: TypeOfEmployment; workMetodology: WorkMetodology; workRole: WorkRole; + workSeniority: WorkSeniority; yearsOfExperience: Scalars['Int']['output']; }; @@ -448,6 +557,11 @@ export type SearchCompaniesInput = { website: InputMaybe; }; +export type SearchPaymentLogsInput = { + endDate: InputMaybe; + startDate: Scalars['DateTime']['input']; +}; + export enum SearchableUserTags { CoreTeam = 'CORE_TEAM', DevTeam = 'DEV_TEAM', @@ -470,13 +584,13 @@ export type TagSearchInput = { /** Representation of a ticket */ export type Ticket = { - currencyId: Maybe; description: Maybe; endDateTime: Maybe; eventId: Scalars['String']['output']; id: Scalars['ID']['output']; name: Scalars['String']['output']; - price: Maybe; + prices: Maybe>; + /** The number of tickets available for this ticket type */ quantity: Maybe; requiresApproval: Maybe; startDateTime: Scalars['DateTime']['output']; @@ -486,9 +600,30 @@ export type Ticket = { export enum TicketApprovalStatus { Approved = 'approved', - Pending = 'pending' + Pending = 'pending', + Rejected = 'rejected' } +export type TicketClaimInput = { + /** A unique key to prevent duplicate requests, it's optional to send, but it's recommended to send it to prevent duplicate requests. If not sent, it will be created by the server. */ + idempotencyUUIDKey: InputMaybe; + purchaseOrder: Array; +}; + +export type TicketCreateInput = { + currencyId: InputMaybe; + description: InputMaybe; + endDateTime: InputMaybe; + eventId: Scalars['String']['input']; + name: Scalars['String']['input']; + price: InputMaybe; + quantity: InputMaybe; + requiresApproval: InputMaybe; + startDateTime: Scalars['DateTime']['input']; + status: InputMaybe; + visibility: InputMaybe; +}; + export type TicketEditInput = { currencyId: InputMaybe; description: InputMaybe; @@ -505,6 +640,7 @@ export type TicketEditInput = { }; export enum TicketPaymentStatus { + NotRequired = 'not_required', Paid = 'paid', Unpaid = 'unpaid' } @@ -516,7 +652,7 @@ export enum TicketRedemptionStatus { export enum TicketStatus { Active = 'active', - Cancelled = 'cancelled' + Inactive = 'inactive' } export enum TicketTemplateStatus { @@ -554,18 +690,17 @@ export type UpdateCompanyInput = { }; export type UpdateSalaryInput = { - amount: Scalars['Int']['input']; - companyId: Scalars['String']['input']; + amount: InputMaybe; confirmationToken: Scalars['String']['input']; - countryCode: Scalars['String']['input']; - currencyId: Scalars['String']['input']; - gender: Gender; - genderOtherText: Scalars['String']['input']; + countryCode: InputMaybe; + currencyCode: InputMaybe; + gender: InputMaybe; + genderOtherText: InputMaybe; salaryId: Scalars['String']['input']; - typeOfEmployment: TypeOfEmployment; - workMetodology: WorkMetodology; - workRoleId: Scalars['String']['input']; - yearsOfExperience: Scalars['Int']['input']; + typeOfEmployment: InputMaybe; + workMetodology: InputMaybe; + workSeniorityAndRoleId: InputMaybe; + yearsOfExperience: InputMaybe; }; /** Representation of a user */ @@ -588,7 +723,22 @@ export type UserTicket = { status: TicketStatus; }; -/** Representation of a workEmail */ +export enum ValidPaymentMethods { + MercadoPago = 'mercado_pago', + Stripe = 'stripe' +} + +/** Representation of a work email associated to the current user */ +export type ValidatedWorkEmail = { + company: Maybe; + confirmationDate: Maybe; + id: Scalars['String']['output']; + isValidated: Scalars['Boolean']['output']; + status: EmailStatus; + workEmail: Scalars['String']['output']; +}; + +/** Representation of a (yet to validate) work email */ export type WorkEmail = { id: Scalars['String']['output']; isValidated: Scalars['Boolean']['output']; @@ -600,12 +750,23 @@ export enum WorkMetodology { Remote = 'remote' } -/** Representation of a workEmail */ +/** Representation of a work role */ export type WorkRole = { - description: Scalars['String']['output']; + description: Maybe; + id: Scalars['String']['output']; + name: Scalars['String']['output']; + seniorities: Array; +}; + +export type WorkRoleSenioritiesInput = { + workRoleId: Scalars['String']['input']; +}; + +/** Representation of a work seniority */ +export type WorkSeniority = { + description: Maybe; id: Scalars['String']['output']; name: Scalars['String']['output']; - seniority: Scalars['String']['output']; }; export type UpdateUserRoleInCommunityInput = { diff --git a/src/api/gql/schema.gql b/src/api/gql/schema.gql index 4365780..04a93f1 100644 --- a/src/api/gql/schema.gql +++ b/src/api/gql/schema.gql @@ -2,6 +2,7 @@ type AllowedCurrency { currency: String! id: String! + validPaymentMethods: ValidPaymentMethods! } enum CommnunityStatus { @@ -40,6 +41,14 @@ enum CompanyStatus { inactive } +"""Representation of a consolidated payment entry log calculation""" +type ConsolidatedPaymentLogEntry { + currencyId: String! + id: String! + platform: String! + totalTransactionAmount: Float! +} + input CreateCommunityInput { description: String! name: String! @@ -64,12 +73,12 @@ input CreateSalaryInput { companyId: String! confirmationToken: String! countryCode: String! - currencyId: String! + currencyCode: String! gender: Gender! genderOtherText: String! typeOfEmployment: TypeOfEmployment! workMetodology: WorkMetodology! - workRoleId: String! + workSeniorityAndRoleId: String! yearsOfExperience: Int! } @@ -83,9 +92,15 @@ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `dat """ scalar DateTime +enum EmailStatus { + confirmed + pending + rejected +} + input EnqueueGoogleAlbumImportInput { albumId: String! - sanityEventInstanceId: String! + sanityEventId: String! token: String! } @@ -98,6 +113,7 @@ type Event { description: String endDateTime: DateTime id: String! + images: [SanityAssetRef!]! latitude: String longitude: String maxAttendees: Int @@ -106,8 +122,15 @@ type Event { startDateTime: DateTime! status: EventStatus! tags: [Tag!]! - tickets(input: EventsTicketsSearchInput): [UserTicket!]! + + """ + List of tickets for sale or redemption for this event. (If you are looking for a user's tickets, use the usersTickets field) + """ + tickets: [Ticket!]! users: [User!]! + + """List of tickets that a user owns for this event.""" + usersTickets(input: EventsTicketsSearchInput): [UserTicket!]! visibility: EventVisibility! } @@ -143,6 +166,11 @@ input EventEditInput { visibility: EventVisibility } +"""Search for tags""" +input EventImageSearch { + eventId: String! +} + enum EventStatus { active inactive @@ -192,6 +220,9 @@ type Mutation { """Cancel a ticket""" cancelUserTicket(userTicketId: String!): UserTicket! + """Attempt to claim a certain ammount of tickets""" + claimUserTicket(input: TicketClaimInput!): RedeemUserTicketResponse! + """Create an community""" createCommunity(input: CreateCommunityInput!): Community! @@ -204,6 +235,9 @@ type Mutation { """Create a salary""" createSalary(input: CreateSalaryInput!): Salary! + """Create a ticket""" + createTicket(input: TicketCreateInput!): Ticket! + """Edit an community""" editCommunity(input: UpdateCommunityInput!): Community! @@ -248,6 +282,35 @@ input MyTicketsSearchInput { status: TicketStatus } +"""Representation of a TicketPrice""" +type Price { + amount: Int! + currency: AllowedCurrency! + id: ID! +} + +"""Representation of a payment log entry""" +type PublicFinanceEntryRef { + createdAt: DateTime! + currencyId: String! + id: String! + platform: String! + transactionAmount: Float! + transactionDate: DateTime +} + +"""Representation of a Purchase Order""" +type PurchaseOrder { + id: ID! + tickets: [UserTicket!]! + totalAmount: Float +} + +input PurchaseOrderInput { + quantity: Int! + ticketId: String! +} + type Query { """Get a list of communities. Filter by name, id, or status""" communities(id: String, name: String, status: CommnunityStatus): [Community!]! @@ -264,6 +327,9 @@ type Query { """Get an event by id""" event(id: String!): Event + """Get a list of images, that are attached to an event""" + eventImages(input: EventImageSearch!): [SanityAssetRef!]! + """Get a list of events. Filter by name, id, status or date""" events(input: EventsSearchInput): [Event!]! @@ -272,6 +338,17 @@ type Query { """Get a list of tickets for the current user""" myTickets(input: MyTicketsSearchInput): [UserTicket!]! + + """Get a list of salaries associated to the user""" + salaries: [Salary!]! + + """ + Search a consolidated payment logs, by date, aggregated by platform and currency_id + """ + searchConsolidatedPaymentLogs(input: SearchPaymentLogsInput!): [ConsolidatedPaymentLogEntry!]! + + """Search on the payment logs by date, and returns a list of payment logs""" + searchPaymentLogs(input: SearchPaymentLogsInput!): [PublicFinanceEntryRef!]! status(name: String): String! """Get a list of tags""" @@ -285,20 +362,37 @@ type Query { """Get a workEmail and check if its validated for this user""" workEmail(email: String!): WorkEmail! + + """Get a list of validated work emails for the user""" + workEmails: [ValidatedWorkEmail!]! + + """Get a a work role's seniorities""" + workRoleSeniorities(input: WorkRoleSenioritiesInput!): [WorkSeniority!]! + + """Get a list of possible work roles""" + workRoles: [WorkRole!]! +} + +type RedeemUserTicketError { + error: Boolean! + errorMessage: String! } +union RedeemUserTicketResponse = PurchaseOrder | RedeemUserTicketError + """Representation of a workEmail""" type Salary { amount: Int! company: Company! countryCode: String! - currency: AllowedCurrency! + currencyCode: String! gender: Gender genderOtherText: String id: String! typeOfEmployment: TypeOfEmployment! workMetodology: WorkMetodology! workRole: WorkRole! + workSeniority: WorkSeniority! yearsOfExperience: Int! } @@ -319,6 +413,11 @@ input SearchCompaniesInput { website: String } +input SearchPaymentLogsInput { + endDate: DateTime + startDate: DateTime! +} + enum SearchableUserTags { CORE_TEAM DEV_TEAM @@ -343,13 +442,14 @@ input TagSearchInput { """Representation of a ticket""" type Ticket { - currencyId: String description: String endDateTime: DateTime eventId: String! id: ID! name: String! - price: Int + prices: [Price!] + + """The number of tickets available for this ticket type""" quantity: Int requiresApproval: Boolean startDateTime: DateTime! @@ -360,6 +460,29 @@ type Ticket { enum TicketApprovalStatus { approved pending + rejected +} + +input TicketClaimInput { + """ + A unique key to prevent duplicate requests, it's optional to send, but it's recommended to send it to prevent duplicate requests. If not sent, it will be created by the server. + """ + idempotencyUUIDKey: String + purchaseOrder: [PurchaseOrderInput!]! +} + +input TicketCreateInput { + currencyId: String + description: String + endDateTime: DateTime + eventId: String! + name: String! + price: Int + quantity: Int + requiresApproval: Boolean + startDateTime: DateTime! + status: TicketTemplateStatus + visibility: TicketTemplateVisibility } input TicketEditInput { @@ -378,6 +501,7 @@ input TicketEditInput { } enum TicketPaymentStatus { + not_required paid unpaid } @@ -389,7 +513,7 @@ enum TicketRedemptionStatus { enum TicketStatus { active - cancelled + inactive } enum TicketTemplateStatus { @@ -427,18 +551,17 @@ input UpdateCompanyInput { } input UpdateSalaryInput { - amount: Int! - companyId: String! + amount: Int confirmationToken: String! - countryCode: String! - currencyId: String! - gender: Gender! - genderOtherText: String! + countryCode: String + currencyCode: String + gender: Gender + genderOtherText: String salaryId: String! - typeOfEmployment: TypeOfEmployment! - workMetodology: WorkMetodology! - workRoleId: String! - yearsOfExperience: Int! + typeOfEmployment: TypeOfEmployment + workMetodology: WorkMetodology + workSeniorityAndRoleId: String + yearsOfExperience: Int } """Representation of a user""" @@ -461,7 +584,22 @@ type UserTicket { status: TicketStatus! } -"""Representation of a workEmail""" +enum ValidPaymentMethods { + mercado_pago + stripe +} + +"""Representation of a work email associated to the current user""" +type ValidatedWorkEmail { + company: Company + confirmationDate: DateTime + id: String! + isValidated: Boolean! + status: EmailStatus! + workEmail: String! +} + +"""Representation of a (yet to validate) work email""" type WorkEmail { id: String! isValidated: Boolean! @@ -473,12 +611,23 @@ enum WorkMetodology { remote } -"""Representation of a workEmail""" +"""Representation of a work role""" type WorkRole { - description: String! + description: String + id: String! + name: String! + seniorities: [WorkSeniority!]! +} + +input WorkRoleSenioritiesInput { + workRoleId: String! +} + +"""Representation of a work seniority""" +type WorkSeniority { + description: String id: String! name: String! - seniority: String! } input updateUserRoleInCommunityInput { From b752bb0a9ffd4135f4c59f1fa7366c64cc1c5bd4 Mon Sep 17 00:00:00 2001 From: Felipe Torres Date: Tue, 2 Apr 2024 07:39:06 -0700 Subject: [PATCH 10/11] chore(Linter): Removing file and fixing some small inter errors --- app/(transition)/(root)/login/page.tsx | 2 ++ app/(transition)/layout.tsx | 4 +++- src/components/Auth/clerk.tsx | 27 -------------------------- 3 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 src/components/Auth/clerk.tsx diff --git a/app/(transition)/(root)/login/page.tsx b/app/(transition)/(root)/login/page.tsx index 48726b5..6e3e00c 100644 --- a/app/(transition)/(root)/login/page.tsx +++ b/app/(transition)/(root)/login/page.tsx @@ -19,6 +19,8 @@ const AuthPage: NextPage = () => { useEffect(() => { const url = new URL(window.location.href); + url.pathname = "/"; + url.hash = ""; setUrl(url.toString()); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/app/(transition)/layout.tsx b/app/(transition)/layout.tsx index 4935a3f..d571ce9 100644 --- a/app/(transition)/layout.tsx +++ b/app/(transition)/layout.tsx @@ -26,7 +26,9 @@ export default function Layout({ children }: { children: React.ReactNode }) { const { data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => {}); + } = supabase.auth.onAuthStateChange((_event, session) => { + // TODO: handle token + }); start().catch((e) => { console.error(e); diff --git a/src/components/Auth/clerk.tsx b/src/components/Auth/clerk.tsx deleted file mode 100644 index 8dcdd20..0000000 --- a/src/components/Auth/clerk.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; -import { ClerkProvider } from "@clerk/clerk-react"; -import React from "react"; - -type Props = { - children: React.ReactNode; -}; - -export const Clerk = ({ children }: Props) => { - return ( - { - const splitted = url.host.split("."); - return [splitted.at(-2), splitted.at(-1)].join("."); - }} - publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!} - > - {children} - - ); -}; From d01d801c25c6f166c200ac7490d291fabf57847b Mon Sep 17 00:00:00 2001 From: "Felipe Torres (fforres)" Date: Wed, 10 Apr 2024 08:54:05 -0700 Subject: [PATCH 11/11] Creating (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AuthProvider usa las sugerencias de supabase para authenticación general (no específicas de NextJS) https://supabase.com/docs/guides/auth/sessions#can-i-use-http-only-cookies-to-store-the-access-and-refresh-tokens https://supabase.com/docs/guides/auth/sessions#sign-out-and-scopes image `` refresca la cookie en cada instancia que se carga (o cada que updatea la sesión mediante el event handler de `onAuthStateChange`. `WIN:` Como está en el Layout, cada ruta recibe su propia instancia de Auth, (Y asi seguimos una de las sugerencias del uso de NextJS al menos 🙏🏼 ) --- app/(transition)/(root)/events/[id]/page.tsx | 20 +-- app/(transition)/(root)/login/page.tsx | 28 +---- app/(transition)/(root)/page.tsx | 8 +- app/(transition)/layout.tsx | 44 ++----- middleware.ts | 19 --- package-lock.json | 123 ++++++++++++------- package.json | 8 +- src/api/ApolloClient.tsx | 18 --- src/api/ApolloClientForRSC.tsx | 28 +++++ src/api/ApolloWrapper.tsx | 1 + src/api/gql/graphql.ts | 51 +++++++- src/api/gql/schema.gql | 61 ++++++++- src/components/Navbar/MainNav.tsx | 31 +---- src/components/Navbar/MobileNav.tsx | 38 ++---- src/components/nav.tsx | 51 ++------ src/components/ui/button.tsx | 1 + src/utils/supabase/AuthProvider/index.tsx | 66 ++++++++++ src/utils/supabase/client.ts | 48 ++++++-- src/utils/supabase/middleware.ts | 64 ---------- src/utils/supabase/server.ts | 38 ------ 20 files changed, 360 insertions(+), 386 deletions(-) delete mode 100644 middleware.ts delete mode 100644 src/api/ApolloClient.tsx create mode 100644 src/api/ApolloClientForRSC.tsx create mode 100644 src/utils/supabase/AuthProvider/index.tsx delete mode 100644 src/utils/supabase/middleware.ts delete mode 100644 src/utils/supabase/server.ts diff --git a/app/(transition)/(root)/events/[id]/page.tsx b/app/(transition)/(root)/events/[id]/page.tsx index 218be9f..367ad46 100644 --- a/app/(transition)/(root)/events/[id]/page.tsx +++ b/app/(transition)/(root)/events/[id]/page.tsx @@ -1,5 +1,4 @@ -import { gql } from "@apollo/client"; -import { getApolloClient } from "@/api/ApolloClient"; +import { getApolloClientForRSC } from "@/api/ApolloClientForRSC"; import { MapPinIcon } from "@heroicons/react/24/outline"; import { Attendees } from "@/components/Event/Attendees/Attendees"; import { Hero } from "@/components/Event/Hero/Hero"; @@ -11,30 +10,23 @@ import { EventType, PageProps } from "./types"; import { GetEventDocument } from "./getEvent.generated"; export default async function Event({ searchParams }: PageProps) { - const c = getApolloClient(); + const c = getApolloClientForRSC(); const { id } = searchParams; const { data, error } = await c.query({ query: GetEventDocument, variables: { - input: id - } + input: id, + }, }); if (error) return

Ocurrió un error cargando el evento

; - + const { event } = data; if (!event) return

No pudimos encontrar el evento que estás buscando

; - const { - address, - community, - description, - name, - startDateTime, - users, - } = event; + const { address, community, description, name, startDateTime, users } = event; const eventDate = new Date(startDateTime).toLocaleString(); diff --git a/app/(transition)/(root)/login/page.tsx b/app/(transition)/(root)/login/page.tsx index 6e3e00c..b2ae3a2 100644 --- a/app/(transition)/(root)/login/page.tsx +++ b/app/(transition)/(root)/login/page.tsx @@ -4,42 +4,24 @@ import { NextPage } from "next"; import { redirect } from "next/navigation"; import { useEffect, useState } from "react"; -import { Session } from "@supabase/supabase-js"; import { Auth } from "@supabase/auth-ui-react"; import { ThemeSupa } from "@supabase/auth-ui-shared"; -import { createClient } from "@/utils/supabase/client"; +import { supabaseClient } from "@/utils/supabase/client"; import { useTheme } from "next-themes"; - -const supabase = createClient(); +import { useIsLoggedIn } from "@/utils/supabase/AuthProvider"; const AuthPage: NextPage = () => { - const [session, setSession] = useState(null); const { resolvedTheme } = useTheme(); const [url, setUrl] = useState(); - useEffect(() => { const url = new URL(window.location.href); url.pathname = "/"; url.hash = ""; setUrl(url.toString()); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - supabase.auth.getSession().then(async ({ data: { session } }) => { - setSession(session); - await supabase.auth.startAutoRefresh(); - }); - - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange(async (_event, session) => { - await supabase.auth.startAutoRefresh(); - setSession(session); - }); - - return () => subscription.unsubscribe(); }, []); - if (session) { + const isLoggedIn = useIsLoggedIn(); + if (isLoggedIn) { redirect("/"); } @@ -55,7 +37,7 @@ const AuthPage: NextPage = () => { ({ query: FetchExampleEventsDocument, }); @@ -18,13 +18,13 @@ export default async function Home() {
-

RSC

+

RSC

{variable.data?.events?.map((event) => (
{event.id}
))}
-

Client fetching

+

Client fetching

diff --git a/app/(transition)/layout.tsx b/app/(transition)/layout.tsx index d571ce9..ad57a18 100644 --- a/app/(transition)/layout.tsx +++ b/app/(transition)/layout.tsx @@ -1,46 +1,16 @@ "use client"; import { AnimatePresence, LazyMotion, domAnimation } from "framer-motion"; -import React, { useEffect, useState } from "react"; +import React from "react"; -import { createClient } from "@/utils/supabase/client"; +import { AuthProvider } from "@/utils/supabase/AuthProvider"; export default function Layout({ children }: { children: React.ReactNode }) { - const supabase = createClient(); - - useEffect(() => { - const start = async () => { - const { data } = await supabase.auth.getSession(); - const accessToken = data?.session?.access_token; - - const localStorageKey = process.env.NEXT_PUBLIC_TOKEN_STORAGE_KEY; - if (!accessToken || !localStorageKey) { - return; - } - - window.localStorage.setItem( - process.env.NEXT_PUBLIC_TOKEN_STORAGE_KEY!, - accessToken, - ); - console.log({ accessToken }); - }; - - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => { - // TODO: handle token - }); - - start().catch((e) => { - console.error(e); - }); - - return () => subscription.unsubscribe(); - }, []); - return ( - - {children} - + + + {children} + + ); } diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 01abe41..0000000 --- a/middleware.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type NextRequest } from 'next/server' -import { updateSession } from '@/utils/supabase/middleware' - -export async function middleware(request: NextRequest) { - return await updateSession(request) -} - -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - * Feel free to modify this pattern to include more paths. - */ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', - ], -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0da9361..f17726d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@apollo/client": "^3.9.0-alpha.4", - "@apollo/experimental-nextjs-app-support": "^0.5.1", + "@apollo/client": "^3.9.11", + "@apollo/experimental-nextjs-app-support": "^0.10.0", "@auth0/nextjs-auth0": "^2.6.3", "@heroicons/react": "^2.0.18", "@next/third-parties": "^14.0.4", @@ -20,7 +20,6 @@ "@radix-ui/react-slot": "^1.0.2", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", - "@supabase/ssr": "^0.1.0", "@supabase/supabase-js": "^2.41.1", "@types/node": "20.4.1", "@types/react": "18.2.14", @@ -28,12 +27,14 @@ "autoprefixer": "10.4.14", "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", + "cookies-next": "^4.1.1", "easymde": "^2.18.0", "eslint-config-next": "13.4.9", "framer-motion": "^10.12.18", "graphiql": "^3.0.6", "graphql": "^16.8.1", "graphql-request": "^6.1.0", + "js-cookie": "^3.0.5", "lucide-react": "^0.265.0", "next": "^14.0.3", "next-themes": "^0.2.1", @@ -58,6 +59,7 @@ "@next/eslint-plugin-next": "^13.5.6", "@tailwindcss/typography": "^0.5.10", "@types/jest": "^29.5.11", + "@types/js-cookie": "^3.0.6", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", "eslint": "^8.44.0", @@ -106,9 +108,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.9.0-alpha.5", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.9.0-alpha.5.tgz", - "integrity": "sha512-KUwJxpNxfsnPZ/+pKx25VKEPnlKoGVrtZoMH/wOJ0CaYpodrY8y3Laa92z/K//YcaHt5RAdedg8I1JrfmzIMFw==", + "version": "3.9.11", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.9.11.tgz", + "integrity": "sha512-H7e9m7cRcFO93tokwzqrsbnfKorkpV24xU30hFH5u2g6B+c1DMo/ouyF/YrBPdrTzqxQCjTUmds/FLmJ7626GA==", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -118,7 +120,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.3", + "rehackt": "0.0.6", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -147,17 +149,28 @@ } } }, - "node_modules/@apollo/experimental-nextjs-app-support": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@apollo/experimental-nextjs-app-support/-/experimental-nextjs-app-support-0.5.2.tgz", - "integrity": "sha512-qUopCHDocCBfL+XDhuPNwiWrJHf7rc75bdDBF2TxZyYwW32wo3vPPac6bGWrYy8lgkJ8K8Z5O56655WQOynFTg==", + "node_modules/@apollo/client-react-streaming": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@apollo/client-react-streaming/-/client-react-streaming-0.10.0.tgz", + "integrity": "sha512-iZ2jYghRS71xFv6O3Js5Ojrrmk4SnIEKwPRKIswQyAtqjHrfvUTyXCDzxrhPcGQe/y7su/XcE7Xp0kOp7yTnlg==", "dependencies": { - "server-only": "^0.0.1", - "superjson": "^1.12.2", + "superjson": "^1.12.2 || ^2.0.0", "ts-invariant": "^0.10.3" }, "peerDependencies": { - "@apollo/client": ">=3.8.0-rc || ^3.8.0 || >=3.9.0-alpha || >=3.9.0-beta || >=3.9.0-rc", + "@apollo/client": "^3.9.6", + "react": "^18" + } + }, + "node_modules/@apollo/experimental-nextjs-app-support": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@apollo/experimental-nextjs-app-support/-/experimental-nextjs-app-support-0.10.0.tgz", + "integrity": "sha512-S3mfZRnAAAaKwA8RNckS4TWYLX5utpmRTwG3WGFtpooYx8QQG8xft0p0a9eTQ53Jrw3nSMJc/wOOsT/5noMCQg==", + "dependencies": { + "@apollo/client-react-streaming": "0.10.0" + }, + "peerDependencies": { + "@apollo/client": "^3.9.6", "next": "^13.4.1 || ^14.0.0", "react": "^18" } @@ -5027,18 +5040,6 @@ "ws": "^8.14.2" } }, - "node_modules/@supabase/ssr": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.1.0.tgz", - "integrity": "sha512-bIVrkqjAK5G3KjkIMKYKtAOlCgRRplEWjrlyRyXSOYtgDieiOhk2ZyNAPsEOa1By9OZVxuX5eAW1fitdnuxayw==", - "dependencies": { - "cookie": "^0.5.0", - "ramda": "^0.29.0" - }, - "peerDependencies": { - "@supabase/supabase-js": "^2.33.1" - } - }, "node_modules/@supabase/storage-js": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz", @@ -5128,6 +5129,11 @@ "@types/tern": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5191,6 +5197,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -6739,6 +6751,29 @@ "node": ">= 0.6" } }, + "node_modules/cookies-next": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.1.1.tgz", + "integrity": "sha512-20QaN0iQSz87Os0BhNg9M71eM++gylT3N5szTlhq2rK6QvXn1FYGPB4eAgU4qFTunbQKhD35zfQ95ZWgzUy3Cg==", + "dependencies": { + "@types/cookie": "^0.6.0", + "@types/node": "^16.10.2", + "cookie": "^0.6.0" + } + }, + "node_modules/cookies-next/node_modules/@types/node": { + "version": "16.18.96", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.96.tgz", + "integrity": "sha512-84iSqGXoO+Ha16j8pRZ/L90vDMKX04QTYMTfYeE1WrjWaZXuchBehGUZEpNgx7JnmlrIHdnABmpjrQjhCnNldQ==" + }, + "node_modules/cookies-next/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -9341,6 +9376,14 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11673,15 +11716,6 @@ } ] }, - "node_modules/ramda": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz", - "integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -11876,9 +11910,9 @@ } }, "node_modules/rehackt": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.3.tgz", - "integrity": "sha512-aBRHudKhOWwsTvCbSoinzq+Lej/7R8e8UoPvLZo5HirZIIBLGAgdG7SL9QpdcBoQ7+3QYPi3lRLknAzXBlhZ7g==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.6.tgz", + "integrity": "sha512-l3WEzkt4ntlEc/IB3/mF6SRgNHA6zfQR7BlGOgBTOmx7IJJXojDASav+NsgXHFjHn+6RmwqsGPFgZpabWpeOdw==", "peerDependencies": { "@types/react": "*", "react": "*" @@ -12213,11 +12247,6 @@ "upper-case-first": "^2.0.2" } }, - "node_modules/server-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -12652,14 +12681,14 @@ } }, "node_modules/superjson": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", - "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", "dependencies": { "copy-anything": "^3.0.2" }, "engines": { - "node": ">=10" + "node": ">=16" } }, "node_modules/supports-color": { diff --git a/package.json b/package.json index 8e03e12..851e5d6 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "node": ">=18.17.0" }, "dependencies": { - "@apollo/client": "^3.9.0-alpha.4", - "@apollo/experimental-nextjs-app-support": "^0.5.1", + "@apollo/client": "^3.9.11", + "@apollo/experimental-nextjs-app-support": "^0.10.0", "@auth0/nextjs-auth0": "^2.6.3", "@heroicons/react": "^2.0.18", "@next/third-parties": "^14.0.4", @@ -29,7 +29,6 @@ "@radix-ui/react-slot": "^1.0.2", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", - "@supabase/ssr": "^0.1.0", "@supabase/supabase-js": "^2.41.1", "@types/node": "20.4.1", "@types/react": "18.2.14", @@ -37,12 +36,14 @@ "autoprefixer": "10.4.14", "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", + "cookies-next": "^4.1.1", "easymde": "^2.18.0", "eslint-config-next": "13.4.9", "framer-motion": "^10.12.18", "graphiql": "^3.0.6", "graphql": "^16.8.1", "graphql-request": "^6.1.0", + "js-cookie": "^3.0.5", "lucide-react": "^0.265.0", "next": "^14.0.3", "next-themes": "^0.2.1", @@ -67,6 +68,7 @@ "@next/eslint-plugin-next": "^13.5.6", "@tailwindcss/typography": "^0.5.10", "@types/jest": "^29.5.11", + "@types/js-cookie": "^3.0.6", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", "eslint": "^8.44.0", diff --git a/src/api/ApolloClient.tsx b/src/api/ApolloClient.tsx deleted file mode 100644 index bcec2c4..0000000 --- a/src/api/ApolloClient.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; -import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc"; - -const { getClient } = registerApolloClient(() => { - return new ApolloClient({ - cache: new InMemoryCache(), - link: new HttpLink({ - // this needs to be an absolute url, as relative urls cannot be used in SSR - uri: process.env.NEXT_PUBLIC_JSCL_API_URL, - fetch, - // you can disable result caching here if you want to - // (this does not work if you are rendering your page with `export const dynamic = "force-static"`) - // fetchOptions: { cache: "no-store" }, - }), - }); -}); - -export const getApolloClient = getClient; diff --git a/src/api/ApolloClientForRSC.tsx b/src/api/ApolloClientForRSC.tsx new file mode 100644 index 0000000..5116fad --- /dev/null +++ b/src/api/ApolloClientForRSC.tsx @@ -0,0 +1,28 @@ +import { COOKIE_NAME } from "@/utils/supabase/client"; +import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; +import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc"; +import { cookies } from "next/headers"; + +const { getClient } = registerApolloClient(() => { + const cookieValue = cookies().get(COOKIE_NAME)?.value ?? ""; + const headers = cookieValue + ? { + headers: { + cookie: `${COOKIE_NAME}=${cookieValue}`, + }, + } + : {}; + console.log("cookieValue", COOKIE_NAME, cookieValue); + console.log("headers", headers); + return new ApolloClient({ + cache: new InMemoryCache(), + link: new HttpLink({ + // this needs to be an absolute url, as relative urls cannot be used in SSR + uri: process.env.NEXT_PUBLIC_JSCL_API_URL, + // fetch, + ...headers, + }), + }); +}); + +export const getApolloClientForRSC = getClient; diff --git a/src/api/ApolloWrapper.tsx b/src/api/ApolloWrapper.tsx index 6d607be..c1fb11f 100644 --- a/src/api/ApolloWrapper.tsx +++ b/src/api/ApolloWrapper.tsx @@ -14,6 +14,7 @@ function makeClient() { // you can disable result caching here if you want to // (this does not work if you are rendering your page with `export const dynamic = "force-static"`) fetchOptions: { cache: "no-store" }, + credentials: "same-origin", // you can override the default `fetchOptions` on a per query basis // via the `context` property on the options passed as a second argument // to an Apollo Client data fetching hook, e.g.: diff --git a/src/api/gql/graphql.ts b/src/api/gql/graphql.ts index 6b48f6f..66a1c17 100644 --- a/src/api/gql/graphql.ts +++ b/src/api/gql/graphql.ts @@ -250,6 +250,8 @@ export type Mutation = { editTicket: Ticket; /** Enqueue images to import */ enqueueGoogleAlbumImport: Scalars['Boolean']['output']; + /** Create a purchase order */ + payForPurchaseOrder: PurchaseOrder; /** Redeem a ticket */ redeemUserTicket: UserTicket; /** Kickoff the email validation flow. This flow will links an email to a user, create a company if it does not exist, and allows filling data for that email's position */ @@ -327,6 +329,11 @@ export type MutationEnqueueGoogleAlbumImportArgs = { }; +export type MutationPayForPurchaseOrderArgs = { + input: PayForPurchaseOrderInput; +}; + + export type MutationRedeemUserTicketArgs = { userTicketId: Scalars['String']['input']; }; @@ -369,6 +376,11 @@ export type MyTicketsSearchInput = { status: InputMaybe; }; +export type PayForPurchaseOrderInput = { + currencyID: Scalars['String']['input']; + purchaseOrderId: Scalars['String']['input']; +}; + /** Representation of a TicketPrice */ export type Price = { amount: Scalars['Int']['output']; @@ -376,6 +388,12 @@ export type Price = { id: Scalars['ID']['output']; }; +export type PricingInputField = { + currencyId: Scalars['String']['input']; + /** The price. But in cents, so for a $10 ticket, you'd pass 1000 (or 10_00), or for 1000 chilean pesos, you'd pass 1000_00 */ + value_in_cents: Scalars['Int']['input']; +}; + /** Representation of a payment log entry */ export type PublicFinanceEntryRef = { createdAt: Scalars['DateTime']['output']; @@ -388,9 +406,12 @@ export type PublicFinanceEntryRef = { /** Representation of a Purchase Order */ export type PurchaseOrder = { + currency: Maybe; + finalPrice: Maybe; id: Scalars['ID']['output']; + paymentLink: Maybe; + status: Maybe; tickets: Array; - totalAmount: Maybe; }; export type PurchaseOrderInput = { @@ -398,6 +419,13 @@ export type PurchaseOrderInput = { ticketId: Scalars['String']['input']; }; +export enum PurchaseOrderStatusEnum { + Cancelled = 'cancelled', + NotRequired = 'not_required', + Paid = 'paid', + Unpaid = 'unpaid' +} + export type Query = { /** Get a list of communities. Filter by name, id, or status */ communities: Array; @@ -588,11 +616,15 @@ export type Ticket = { endDateTime: Maybe; eventId: Scalars['String']['output']; id: Scalars['ID']['output']; + /** Whether or not the ticket is free */ + isFree: Scalars['Boolean']['output']; + /** Whether or not the ticket has an unlimited quantity. This is reserved for things loike online events. */ + isUnlimited: Scalars['Boolean']['output']; name: Scalars['String']['output']; prices: Maybe>; /** The number of tickets available for this ticket type */ quantity: Maybe; - requiresApproval: Maybe; + requiresApproval: Scalars['Boolean']['output']; startDateTime: Scalars['DateTime']['output']; status: TicketTemplateStatus; visibility: TicketTemplateVisibility; @@ -600,6 +632,7 @@ export type Ticket = { export enum TicketApprovalStatus { Approved = 'approved', + NotRequired = 'not_required', Pending = 'pending', Rejected = 'rejected' } @@ -611,35 +644,40 @@ export type TicketClaimInput = { }; export type TicketCreateInput = { - currencyId: InputMaybe; description: InputMaybe; endDateTime: InputMaybe; eventId: Scalars['String']['input']; + /** If the ticket is free, the price submitted will be ignored. */ + isFree: Scalars['Boolean']['input']; name: Scalars['String']['input']; - price: InputMaybe; + prices: InputMaybe>; quantity: InputMaybe; requiresApproval: InputMaybe; startDateTime: Scalars['DateTime']['input']; status: InputMaybe; + /** If provided, quantity must not be passed. This is for things like online events where there is no limit to the amount of tickets that can be sold. */ + unlimitedTickets: Scalars['Boolean']['input']; visibility: InputMaybe; }; export type TicketEditInput = { - currencyId: InputMaybe; description: InputMaybe; endDateTime: InputMaybe; eventId: InputMaybe; name: InputMaybe; - price: InputMaybe; + prices: InputMaybe; quantity: InputMaybe; requiresApproval: InputMaybe; startDateTime: InputMaybe; status: InputMaybe; ticketId: Scalars['String']['input']; + /** If provided, quantity must not be passed. This is for things like online events where there is no limit to the amount of tickets that can be sold. */ + unlimitedTickets: InputMaybe; visibility: InputMaybe; }; export enum TicketPaymentStatus { + Cancelled = 'cancelled', NotRequired = 'not_required', Paid = 'paid', Unpaid = 'unpaid' @@ -652,6 +690,7 @@ export enum TicketRedemptionStatus { export enum TicketStatus { Active = 'active', + Expired = 'expired', Inactive = 'inactive' } diff --git a/src/api/gql/schema.gql b/src/api/gql/schema.gql index 04a93f1..2547319 100644 --- a/src/api/gql/schema.gql +++ b/src/api/gql/schema.gql @@ -250,6 +250,9 @@ type Mutation { """Enqueue images to import""" enqueueGoogleAlbumImport(input: EnqueueGoogleAlbumImportInput!): Boolean! + """Create a purchase order""" + payForPurchaseOrder(input: PayForPurchaseOrderInput!): PurchaseOrder! + """Redeem a ticket""" redeemUserTicket(userTicketId: String!): UserTicket! @@ -282,6 +285,11 @@ input MyTicketsSearchInput { status: TicketStatus } +input PayForPurchaseOrderInput { + currencyID: String! + purchaseOrderId: String! +} + """Representation of a TicketPrice""" type Price { amount: Int! @@ -289,6 +297,15 @@ type Price { id: ID! } +input PricingInputField { + currencyId: String! + + """ + The price. But in cents, so for a $10 ticket, you'd pass 1000 (or 10_00), or for 1000 chilean pesos, you'd pass 1000_00 + """ + value_in_cents: Int! +} + """Representation of a payment log entry""" type PublicFinanceEntryRef { createdAt: DateTime! @@ -301,9 +318,12 @@ type PublicFinanceEntryRef { """Representation of a Purchase Order""" type PurchaseOrder { + currency: AllowedCurrency + finalPrice: Float id: ID! + paymentLink: String + status: PurchaseOrderStatusEnum tickets: [UserTicket!]! - totalAmount: Float } input PurchaseOrderInput { @@ -311,6 +331,13 @@ input PurchaseOrderInput { ticketId: String! } +enum PurchaseOrderStatusEnum { + cancelled + not_required + paid + unpaid +} + type Query { """Get a list of communities. Filter by name, id, or status""" communities(id: String, name: String, status: CommnunityStatus): [Community!]! @@ -446,12 +473,20 @@ type Ticket { endDateTime: DateTime eventId: String! id: ID! + + """Whether or not the ticket is free""" + isFree: Boolean! + + """ + Whether or not the ticket has an unlimited quantity. This is reserved for things loike online events. + """ + isUnlimited: Boolean! name: String! prices: [Price!] """The number of tickets available for this ticket type""" quantity: Int - requiresApproval: Boolean + requiresApproval: Boolean! startDateTime: DateTime! status: TicketTemplateStatus! visibility: TicketTemplateVisibility! @@ -459,6 +494,7 @@ type Ticket { enum TicketApprovalStatus { approved + not_required pending rejected } @@ -472,35 +508,47 @@ input TicketClaimInput { } input TicketCreateInput { - currencyId: String description: String endDateTime: DateTime eventId: String! + + """If the ticket is free, the price submitted will be ignored.""" + isFree: Boolean! name: String! - price: Int + prices: [PricingInputField!] quantity: Int requiresApproval: Boolean startDateTime: DateTime! status: TicketTemplateStatus + + """ + If provided, quantity must not be passed. This is for things like online events where there is no limit to the amount of tickets that can be sold. + """ + unlimitedTickets: Boolean! visibility: TicketTemplateVisibility } input TicketEditInput { - currencyId: String description: String endDateTime: DateTime eventId: String name: String - price: Int + prices: PricingInputField quantity: Int requiresApproval: Boolean startDateTime: DateTime status: TicketTemplateStatus ticketId: String! + + """ + If provided, quantity must not be passed. This is for things like online events where there is no limit to the amount of tickets that can be sold. + """ + unlimitedTickets: Boolean visibility: TicketTemplateVisibility } enum TicketPaymentStatus { + cancelled not_required paid unpaid @@ -513,6 +561,7 @@ enum TicketRedemptionStatus { enum TicketStatus { active + expired inactive } diff --git a/src/components/Navbar/MainNav.tsx b/src/components/Navbar/MainNav.tsx index 7acfafb..6de44aa 100644 --- a/src/components/Navbar/MainNav.tsx +++ b/src/components/Navbar/MainNav.tsx @@ -3,40 +3,17 @@ import Link from "next/link"; import { NavBarProps } from "./types"; import { NavbarItem } from "./NavbarItem"; import { buttonVariants } from "../ui/button"; -import { useEffect, useState } from "react"; -import { User } from "@supabase/supabase-js"; -import { createClient } from "@/utils/supabase/client"; +import { useIsAuthReady, useIsLoggedIn } from "@/utils/supabase/AuthProvider"; export function MainNav({ items }: NavBarProps) { - const supabase = createClient(); - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const getUser = async () => { - const { data } = await supabase.auth.getSession(); - setLoading(false); - setUser(data?.session?.user ?? null); - }; - - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null); - }); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - getUser(); - - return () => subscription.unsubscribe(); - }, []); - + const user = useIsLoggedIn(); + const isReady = useIsAuthReady(); return (