diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..9d6e37c --- /dev/null +++ b/deno.lock @@ -0,0 +1,199 @@ +{ + "version": "5", + "redirects": { + "https://esm.sh/@supabase/node-fetch@^2.6.13?target=denonext": "https://esm.sh/@supabase/node-fetch@2.6.15?target=denonext", + "https://esm.sh/@supabase/node-fetch@^2.6.14?target=denonext": "https://esm.sh/@supabase/node-fetch@2.6.15?target=denonext", + "https://esm.sh/@supabase/supabase-js@2": "https://esm.sh/@supabase/supabase-js@2.57.4", + "https://esm.sh/@types/boolbase@~1.0.3/index.d.ts": "https://esm.sh/@types/boolbase@1.0.3/index.d.ts", + "https://esm.sh/boolbase@^1.0.0?target=denonext": "https://esm.sh/boolbase@1.0.0?target=denonext", + "https://esm.sh/cheerio-select@^2.1.0?target=denonext": "https://esm.sh/cheerio-select@2.1.0?target=denonext", + "https://esm.sh/css-select@^5.1.0?target=denonext": "https://esm.sh/css-select@5.2.2?target=denonext", + "https://esm.sh/css-what@^6.1.0?target=denonext": "https://esm.sh/css-what@6.2.2?target=denonext", + "https://esm.sh/dom-serializer@^2.0.0?target=denonext": "https://esm.sh/dom-serializer@2.0.0?target=denonext", + "https://esm.sh/domelementtype@^2.3.0?target=denonext": "https://esm.sh/domelementtype@2.3.0?target=denonext", + "https://esm.sh/domhandler@^5.0.3?target=denonext": "https://esm.sh/domhandler@5.0.3?target=denonext", + "https://esm.sh/domutils@^3.0.1?target=denonext": "https://esm.sh/domutils@3.2.2?target=denonext", + "https://esm.sh/entities@^4.2.0?target=denonext": "https://esm.sh/entities@4.5.0?target=denonext", + "https://esm.sh/entities@^4.4.0/lib/decode?target=denonext": "https://esm.sh/entities@4.5.0/lib/decode?target=denonext", + "https://esm.sh/entities@^6.0.0/decode?target=denonext": "https://esm.sh/entities@6.0.1/decode?target=denonext", + "https://esm.sh/entities@^6.0.0/escape?target=denonext": "https://esm.sh/entities@6.0.1/escape?target=denonext", + "https://esm.sh/htmlparser2@^8.0.1?target=denonext": "https://esm.sh/htmlparser2@8.0.2?target=denonext", + "https://esm.sh/nth-check@^2.0.1?target=denonext": "https://esm.sh/nth-check@2.1.1?target=denonext", + "https://esm.sh/parse5-htmlparser2-tree-adapter@^7.0.0?target=denonext": "https://esm.sh/parse5-htmlparser2-tree-adapter@7.1.0?target=denonext", + "https://esm.sh/parse5@^7.0.0?target=denonext": "https://esm.sh/parse5@7.3.0?target=denonext", + "https://esm.sh/tr46@~0.0.3?target=denonext": "https://esm.sh/tr46@0.0.3?target=denonext", + "https://esm.sh/webidl-conversions@^3.0.0?target=denonext": "https://esm.sh/webidl-conversions@3.0.1?target=denonext", + "https://esm.sh/whatwg-url@^5.0.0?target=denonext": "https://esm.sh/whatwg-url@5.0.0?target=denonext" + }, + "remote": { + "https://deno.land/std@0.168.0/async/abortable.ts": "80b2ac399f142cc528f95a037a7d0e653296352d95c681e284533765961de409", + "https://deno.land/std@0.168.0/async/deadline.ts": "2c2deb53c7c28ca1dda7a3ad81e70508b1ebc25db52559de6b8636c9278fd41f", + "https://deno.land/std@0.168.0/async/debounce.ts": "60301ffb37e730cd2d6f9dadfd0ecb2a38857681bd7aaf6b0a106b06e5210a98", + "https://deno.land/std@0.168.0/async/deferred.ts": "77d3f84255c3627f1cc88699d8472b664d7635990d5358c4351623e098e917d6", + "https://deno.land/std@0.168.0/async/delay.ts": "5a9bfba8de38840308a7a33786a0155a7f6c1f7a859558ddcec5fe06e16daf57", + "https://deno.land/std@0.168.0/async/mod.ts": "7809ad4bb223e40f5fdc043e5c7ca04e0e25eed35c32c3c32e28697c553fa6d9", + "https://deno.land/std@0.168.0/async/mux_async_iterator.ts": "770a0ff26c59f8bbbda6b703a2235f04e379f73238e8d66a087edc68c2a2c35f", + "https://deno.land/std@0.168.0/async/pool.ts": "6854d8cd675a74c73391c82005cbbe4cc58183bddcd1fbbd7c2bcda42b61cf69", + "https://deno.land/std@0.168.0/async/retry.ts": "e8e5173623915bbc0ddc537698fa418cf875456c347eda1ed453528645b42e67", + "https://deno.land/std@0.168.0/async/tee.ts": "3a47cc4e9a940904fd4341f0224907e199121c80b831faa5ec2b054c6d2eff5e", + "https://deno.land/std@0.168.0/http/server.ts": "e99c1bee8a3f6571ee4cdeb2966efad465b8f6fe62bec1bdb59c1f007cc4d155", + "https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea", + "https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", + "https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", + "https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", + "https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", + "https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1", + "https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", + "https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", + "https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774", + "https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", + "https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", + "https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", + "https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e", + "https://esm.sh/@supabase/auth-js@2.71.1/denonext/auth-js.mjs": "d55f67342e652b8bdce35b0ff13ad5cc294b7e96dbd68f859b464b07c6864967", + "https://esm.sh/@supabase/functions-js@2.4.6/denonext/functions-js.mjs": "d6cc049a0430f428ff0b71a0d3c48d45a243ddd48c68febcdb5cb8a02476a1dc", + "https://esm.sh/@supabase/node-fetch@2.6.15/denonext/node-fetch.mjs": "0bae9052231f4f6dbccc7234d05ea96923dbf967be12f402764580b6bf9f713d", + "https://esm.sh/@supabase/node-fetch@2.6.15?target=denonext": "4d28c4ad97328403184353f68434f2b6973971507919e9150297413664919cf3", + "https://esm.sh/@supabase/postgrest-js@1.21.4/denonext/postgrest-js.mjs": "c3769b11ef02debc78ecf6ab4e152d3cf7dbd05bbbafeb72c160e76cc57cda3c", + "https://esm.sh/@supabase/realtime-js@2.15.5/denonext/realtime-js.mjs": "518bdc73c29b502ba4dcf7ce2dff0ff8c1cbd8e5978f7ea2435af8214ea45dd5", + "https://esm.sh/@supabase/storage-js@2.12.1/denonext/storage-js.mjs": "7a5a47546486972c0627b620e7413300b4e82ac6e26b53d2c31933e13c2d652e", + "https://esm.sh/@supabase/supabase-js@2.57.4": "05a369085eb4a4c99d85ccece97f0cf1e05357122e0e74373da1f0e91b014902", + "https://esm.sh/@supabase/supabase-js@2.57.4/denonext/supabase-js.mjs": "b31f4ec51272218b68cfdcef9de5aa7abd0f1da1262fa0b9377c62eb18fe494b", + "https://esm.sh/boolbase@1.0.0/denonext/boolbase.mjs": "70e9521b9532b5e4dc0c807422529b15b4452663dbdb70dff9c7b65d0ff2e3cb", + "https://esm.sh/boolbase@1.0.0?target=denonext": "5d10bc2e0fb13eedfc6859bffbeb5a6f08679797fa8740c7d821841c2e22945f", + "https://esm.sh/cheerio-select@2.1.0/denonext/cheerio-select.mjs": "755b7da4011b67a75d1140d76c503cd6929c7213454debf5b6ebc086b73fa9d9", + "https://esm.sh/cheerio-select@2.1.0?target=denonext": "ae26d1996b4bb1d701cb7095e1c2ed7310e5fe88c3786efc26ceb6168ce1513d", + "https://esm.sh/cheerio@1.0.0-rc.12": "fce7bbfff7de7d2c635a798ea80e9a8beb5284c394c79d76afbe6d7b9675e7c0", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/cheerio.mjs": "b4ca825480bc25536b37570eacb6693d4c6d2371033ff2e5e32ceedc8f01e42f", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/lib/esm/load.mjs": "f5493a87fd62b2c4e19185a1e1a3ed93d5b2becfb7b46bfccc9ac16943883821", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/lib/esm/options.mjs": "382ade1b80a105b9d83e74f4c87e2cb27ebc0b3304c7b7094443b2f191f7b881", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/lib/esm/parse.mjs": "924fbfd7fa9528fb593ee0ad0f678196c076f1f3d733cbbb0a19f718e5796b5f", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/lib/esm/static.mjs": "5b6fe5cefd4a13f0692930f81e5a3667ddf45051910bf74276fb584605aacbd7", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/lib/esm/types.mjs": "548363e175a73fe23431f9959f0c4e942d9f9f107dbb5a3367f6a4e4f4129beb", + "https://esm.sh/cheerio@1.0.0-rc.12/denonext/lib/utils.mjs": "53ba8383160d9a7cee7d7c8db9b680ba276c2439f391fc8395b64eac5d5c5a35", + "https://esm.sh/css-select@5.2.2/denonext/css-select.mjs": "db6e191df366250412483170f6cc25b8416b86b7cbfc85b386de2aa92712924b", + "https://esm.sh/css-select@5.2.2?target=denonext": "6a1bffb076b7b4260cd1c62d3be28be32fdc7c9260a22f2402f218cb12beb440", + "https://esm.sh/css-what@6.2.2/denonext/css-what.mjs": "9c9b079c45f30d5392006f8225f9322564898dd23888e9e6740e25955d37e204", + "https://esm.sh/css-what@6.2.2?target=denonext": "74e16b118fb7045d5c136d4deaa3473627c8d10eb1fd7d0ab29ae5f588bec979", + "https://esm.sh/dom-serializer@2.0.0/denonext/dom-serializer.mjs": "545028b1d2c25bae5cbfe6930a28a2e4f7f05e1a0d09bbd0f3f5f9a33df8e3bd", + "https://esm.sh/dom-serializer@2.0.0?target=denonext": "1626b2b8326556ea2816b5f9bf7522bc9581d545fd9ad117c066ab7a5ff1fb89", + "https://esm.sh/domelementtype@2.3.0/denonext/domelementtype.mjs": "4f3b57348729cd517560139eb1969ca2fe9cc58c5188abe56e7336d5cb557cc0", + "https://esm.sh/domelementtype@2.3.0?target=denonext": "2beb2a1e3d18892a9b00ef9528811b93f613a77d2b6fb25376ec0f109ac48a4f", + "https://esm.sh/domhandler@5.0.3/denonext/domhandler.mjs": "3fb258a3d79bc9066a568bb6b09ce946d1fcfa2636a24ae80a4db220956e0873", + "https://esm.sh/domhandler@5.0.3?target=denonext": "298fde249b7bff9e80667cfe643e7d4b390871b77b0928d086ce4c0b8fc570e2", + "https://esm.sh/domutils@3.2.2/denonext/domutils.mjs": "f0b4e80e73810ed6f3d8c4e1822feef89208f32c88b6024a84328d02f5f77c40", + "https://esm.sh/domutils@3.2.2?target=denonext": "7e487176c61dfd1dfdbcfd1195e7329a64f53421511561b69c570a6cff0a6167", + "https://esm.sh/entities@4.5.0/denonext/entities.mjs": "4a9306e4021ae1079e83b5db26e1678c536fa69c8f2839802bc3cc43282cef08", + "https://esm.sh/entities@4.5.0/denonext/lib/decode.mjs": "ef22e25f6bca668e40c4f7d4ecaebe2172a833a18372d55b54f997d0d8702dcd", + "https://esm.sh/entities@4.5.0/denonext/lib/escape.mjs": "116aef78e5ff05efa6f79851b8b59da025ab88f5c25d2262f73df98f4d57c3fa", + "https://esm.sh/entities@4.5.0/lib/decode?target=denonext": "488bc8401a0c85a76527d61a41352c5371904aeda57a136eb10ccfadcd2f7c8c", + "https://esm.sh/entities@4.5.0?target=denonext": "f6bc559c07f40e94b3ef50f0b24e2666a2258db3b6697bf4da8fd2fc014ef7a1", + "https://esm.sh/entities@6.0.1/decode?target=denonext": "3ccc9b5e285ac182223bec6c9e053ff17814f4d27a31b8abafe35d3b684faaa7", + "https://esm.sh/entities@6.0.1/denonext/decode.mjs": "0e11dc867c49cd73eaa3de858276b02727bf6a3e1e5a84be72722cba08697b7d", + "https://esm.sh/entities@6.0.1/denonext/escape.mjs": "f23f7faf0499133a54a93a5dc4276f08793b2bb36b539b1bacddcc3b1e746aca", + "https://esm.sh/entities@6.0.1/escape?target=denonext": "c3df42c65816226666e5a0560e68e3c058a184684488a26b3dedce05eaa329e2", + "https://esm.sh/htmlparser2@8.0.2/denonext/htmlparser2.mjs": "c0be0f190e625b82e88378875016f820a38d586e9c885d37e3dd2073a4f0fdfb", + "https://esm.sh/htmlparser2@8.0.2/denonext/lib/esm/Parser.mjs": "d58fb2f87f8fead8e7ac03c544690908b4db21ab0513415b0cfe9311ab31aaa3", + "https://esm.sh/htmlparser2@8.0.2/denonext/lib/esm/Tokenizer.mjs": "09109e601c7acd75b99c2a34e53e339a46301d9937e1495eaca8f9019a7b3040", + "https://esm.sh/htmlparser2@8.0.2?target=denonext": "fd3edaa58a00e79f11b3510cc3930c9f5345486cdcc52c0afe24bbb36aece028", + "https://esm.sh/nth-check@2.1.1/denonext/nth-check.mjs": "2b0541d4564b27c31b37b006329c64036bee04a4c8f14e8357037fd7d35ecfe4", + "https://esm.sh/nth-check@2.1.1?target=denonext": "689135c5e0e825a2a89058636f1b3a497040597c17de9107c703e9d764d22e25", + "https://esm.sh/parse5-htmlparser2-tree-adapter@7.1.0/denonext/parse5-htmlparser2-tree-adapter.mjs": "7bfd000e678a20d0648da01bf5a9bc85188b3d10e4e079525324d336d9a4a4a2", + "https://esm.sh/parse5-htmlparser2-tree-adapter@7.1.0?target=denonext": "c3f6c7adc65cd1bc13b5fb95d1cd40cdb1250d49488c5252156d3fde0947b0e4", + "https://esm.sh/parse5@7.3.0/denonext/parse5.mjs": "39564d89f13b5d701ac8f869caea8660ada421fcd19ef2234adfd935cf7cf27e", + "https://esm.sh/parse5@7.3.0?target=denonext": "898a8cf4c02510b1cd0f90c9dffdfe9229f31f2dec425310da4404d472137f2e", + "https://esm.sh/tr46@0.0.3/denonext/tr46.mjs": "5753ec0a99414f4055f0c1f97691100f13d88e48a8443b00aebb90a512785fa2", + "https://esm.sh/tr46@0.0.3?target=denonext": "19cb9be0f0d418a0c3abb81f2df31f080e9540a04e43b0f699bce1149cba0cbb", + "https://esm.sh/webidl-conversions@3.0.1/denonext/webidl-conversions.mjs": "54b5c2d50a294853c4ccebf9d5ed8988c94f4e24e463d84ec859a866ea5fafec", + "https://esm.sh/webidl-conversions@3.0.1?target=denonext": "4e20318d50528084616c79d7b3f6e7f0fe7b6d09013bd01b3974d7448d767e29", + "https://esm.sh/whatwg-url@5.0.0/denonext/whatwg-url.mjs": "29b16d74ee72624c915745bbd25b617cfd2248c6af0f5120d131e232a9a9af79", + "https://esm.sh/whatwg-url@5.0.0?target=denonext": "f001a2cadf81312d214ca330033f474e74d81a003e21e8c5d70a1f46dc97b02d" + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@hookform/resolvers@^3.9.0", + "npm:@playwright/test@^1.54.1", + "npm:@radix-ui/react-accordion@^1.2.0", + "npm:@radix-ui/react-alert-dialog@^1.1.1", + "npm:@radix-ui/react-aspect-ratio@^1.1.0", + "npm:@radix-ui/react-avatar@^1.1.0", + "npm:@radix-ui/react-checkbox@^1.1.1", + "npm:@radix-ui/react-collapsible@^1.1.0", + "npm:@radix-ui/react-context-menu@^2.2.1", + "npm:@radix-ui/react-dialog@^1.1.2", + "npm:@radix-ui/react-dropdown-menu@^2.1.1", + "npm:@radix-ui/react-hover-card@^1.1.1", + "npm:@radix-ui/react-label@^2.1.0", + "npm:@radix-ui/react-menubar@^1.1.1", + "npm:@radix-ui/react-navigation-menu@^1.2.0", + "npm:@radix-ui/react-popover@^1.1.1", + "npm:@radix-ui/react-progress@^1.1.0", + "npm:@radix-ui/react-radio-group@^1.2.0", + "npm:@radix-ui/react-scroll-area@^1.1.0", + "npm:@radix-ui/react-select@^2.1.1", + "npm:@radix-ui/react-separator@^1.1.0", + "npm:@radix-ui/react-slider@^1.2.0", + "npm:@radix-ui/react-slot@^1.1.0", + "npm:@radix-ui/react-switch@^1.1.0", + "npm:@radix-ui/react-tabs@^1.1.0", + "npm:@radix-ui/react-toast@^1.2.1", + "npm:@radix-ui/react-toggle-group@^1.1.0", + "npm:@radix-ui/react-toggle@^1.1.0", + "npm:@radix-ui/react-tooltip@^1.1.4", + "npm:@supabase/supabase-js@^2.50.0", + "npm:@tailwindcss/line-clamp@~0.4.4", + "npm:@tailwindcss/typography@~0.5.15", + "npm:@tanstack/query-async-storage-persister@^5.86.0", + "npm:@tanstack/react-query-devtools@^5.81.2", + "npm:@tanstack/react-query-persist-client@^5.85.9", + "npm:@tanstack/react-query@^5.56.2", + "npm:@types/node@^22.5.5", + "npm:@types/react-dom@^18.3.0", + "npm:@types/react@^18.3.3", + "npm:@vitejs/plugin-react-swc@^3.5.0", + "npm:autoprefixer@^10.4.20", + "npm:class-variance-authority@~0.7.1", + "npm:clsx@^2.1.1", + "npm:cmdk@1", + "npm:date-fns-tz@^3.2.0", + "npm:date-fns@^4.1.0", + "npm:embla-carousel-react@^8.3.0", + "npm:framer-motion@^12.23.12", + "npm:globals@^15.9.0", + "npm:husky@^9.1.7", + "npm:idb@^8.0.3", + "npm:input-otp@^1.2.4", + "npm:lint-staged@^16.1.4", + "npm:lovable-tagger@^1.1.7", + "npm:lucide-react@0.462", + "npm:next-themes@0.3", + "npm:oxlint@^1.11.1", + "npm:postcss@^8.4.47", + "npm:prettier@^3.6.2", + "npm:react-day-picker@^9.8.0", + "npm:react-dom@^18.3.1", + "npm:react-hook-form@^7.53.0", + "npm:react-resizable-panels@^2.1.3", + "npm:react-router-dom@^6.26.2", + "npm:react@^18.3.1", + "npm:recharts@^2.12.7", + "npm:sonner@^1.5.0", + "npm:supabase@^2.33.9", + "npm:tailwind-merge@^2.5.2", + "npm:tailwindcss-animate@^1.0.7", + "npm:tailwindcss@^3.4.11", + "npm:tsx@^4.20.3", + "npm:typescript@^5.5.3", + "npm:vaul@~0.9.3", + "npm:vite-plugin-pwa@^1.0.1", + "npm:vite@^5.4.1", + "npm:workbox-background-sync@^7.3.0", + "npm:workbox-precaching@^7.3.0", + "npm:workbox-routing@^7.3.0", + "npm:workbox-strategies@^7.3.0", + "npm:zod@^3.23.8" + ] + } + } +} diff --git a/package.json b/package.json index a21d63f..75a8ddc 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "date-fns-tz": "^3.2.0", "embla-carousel-react": "^8.3.0", "franc": "^6.2.0", + "framer-motion": "^12.23.12", "idb": "^8.0.3", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23289a3..6d56fc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: embla-carousel-react: specifier: ^8.3.0 version: 8.6.0(react@18.3.1) + framer-motion: + specifier: ^12.23.12 + version: 12.23.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1) franc: specifier: ^6.2.0 version: 6.2.0 @@ -2813,6 +2816,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@12.23.22: + resolution: {integrity: sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==} + 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 + franc@6.2.0: resolution: {integrity: sha512-rcAewP7PSHvjq7Kgd7dhj82zE071kX5B4W1M4ewYMf/P+i6YsDQmj62Xz3VQm9zyUzUXwhIde/wHLGCMrM+yGg==} @@ -3272,6 +3289,12 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + motion-dom@12.23.21: + resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -6967,6 +6990,15 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@12.23.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.23.21 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + franc@6.2.0: dependencies: trigram-utils: 2.0.1 @@ -7437,6 +7469,12 @@ snapshots: dependencies: minipass: 7.1.2 + motion-dom@12.23.21: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + mrmime@2.0.1: {} ms@2.1.3: {} diff --git a/src/components/router/EditionRoutes.tsx b/src/components/router/EditionRoutes.tsx index e0dab06..da3d442 100644 --- a/src/components/router/EditionRoutes.tsx +++ b/src/components/router/EditionRoutes.tsx @@ -1,6 +1,7 @@ import { Navigate, Route } from "react-router-dom"; import EditionView from "@/pages/EditionView/EditionView"; import { SetDetails } from "@/pages/SetDetails"; +import { ExploreSetPage } from "@/pages/ExploreSetPage/ExploreSetPage"; // Tab components import { ArtistsTab } from "@/pages/EditionView/tabs/ArtistsTab/ArtistsTab"; @@ -28,6 +29,10 @@ export function createEditionRoutes({ ? () => : SetDetails; + const ExploreComponent = WrapperComponent + ? () => + : ExploreSetPage; + return [ }> {/* Nested tab routes */} @@ -47,5 +52,10 @@ export function createEditionRoutes({ path={`${basePath}/sets/:setSlug`} element={} />, + } + />, ]; } diff --git a/src/hooks/mutations/useSyncSoundCloudDataMutation.ts b/src/hooks/mutations/useSyncSoundCloudDataMutation.ts new file mode 100644 index 0000000..e0652c0 --- /dev/null +++ b/src/hooks/mutations/useSyncSoundCloudDataMutation.ts @@ -0,0 +1,42 @@ +import { useMutation } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { toast } from "sonner"; + +type SyncResponse = { + message: string; + artistsToProcess: number; + startedAt: string; +}; + +async function syncSoundCloudData(): Promise { + const { data, error } = await supabase.functions.invoke("sync-artist-data", { + body: {}, + }); + + if (error) { + throw new Error(error.message || "Failed to start SoundCloud sync"); + } + + return data; +} + +export function useSyncSoundCloudDataMutation() { + return useMutation({ + mutationFn: syncSoundCloudData, + onSuccess: (data) => { + toast.success( + `SoundCloud sync started! Processing ${data.artistsToProcess} artists.`, + { + description: + "The sync is running in the background. Check back in a few minutes.", + duration: 5000, + }, + ); + }, + onError: (error) => { + toast.error("Failed to start SoundCloud sync", { + description: error.message, + }); + }, + }); +} diff --git a/src/hooks/queries/artists/useArtists.ts b/src/hooks/queries/artists/useArtists.ts index f32b357..bd0b6dc 100644 --- a/src/hooks/queries/artists/useArtists.ts +++ b/src/hooks/queries/artists/useArtists.ts @@ -4,6 +4,7 @@ import type { Database } from "@/integrations/supabase/types"; export type Artist = Database["public"]["Tables"]["artists"]["Row"] & { artist_music_genres: { music_genre_id: string }[] | null; + soundcloud_followers?: number; }; // Query key factory @@ -34,7 +35,27 @@ async function fetchArtists(): Promise { throw new Error("Failed to fetch artists"); } - return data || []; + const { data: soundcloudData, error: soundcloudError } = await supabase + .from("soundcloud") + .select("artist_id, followers_count"); + + if (soundcloudError) { + console.error("Error fetching soundcloud data:", soundcloudError); + throw new Error("Failed to fetch soundcloud data"); + } + + const soundcloudMap = new Map( + soundcloudData?.map((sc) => [sc.artist_id, sc.followers_count]) || [], + ); + + return ( + data.map((artist) => { + return { + ...artist, + soundcloud_followers: soundcloudMap.get(artist.id) || 0, + }; + }) || [] + ); } // Hook diff --git a/src/hooks/useArtistSoundcloudPlaylist.ts b/src/hooks/useArtistSoundcloudPlaylist.ts new file mode 100644 index 0000000..1de8e7f --- /dev/null +++ b/src/hooks/useArtistSoundcloudPlaylist.ts @@ -0,0 +1,130 @@ +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { FunctionsHttpError } from "@supabase/supabase-js"; + +// Enhanced error class that includes error codes +export class SoundCloudError extends Error { + public readonly code: string; + + constructor(message: string, code: string = "UNKNOWN_ERROR") { + super(message); + this.name = "SoundCloudError"; + this.code = code; + } +} + +// Type guard for SoundCloudError +export function isSoundCloudError(error: unknown): error is SoundCloudError { + return error instanceof SoundCloudError; +} + +// SoundCloud API response types +interface SoundCloudUser { + id: number; + username: string; + permalink_url: string; +} + +interface SoundCloudPlaylist { + id: number; + title: string; + description: string | null; + permalink_url: string; + artwork_url: string | null; + user: SoundCloudUser; + track_count: number; + tracks?: SoundCloudTrack[]; + likes_count?: number; + reposts_count?: number; + created_at: string; +} + +interface SoundCloudTrack { + id: number; + title: string; + permalink_url: string; + stream_url?: string; + duration: number; + artwork_url: string | null; +} + +interface UseArtistSoundcloudPlaylistOptions { + soundcloudUrl: string; + enabled?: boolean; +} + +async function fetchArtistPlaylist( + soundcloudUrl: string, +): Promise { + console.log( + "[fetchArtistPlaylist] Calling edge function for:", + soundcloudUrl, + ); + + const { data, error } = await supabase.functions.invoke( + "get-artist-soundcloud-playlist", + { + body: { url: soundcloudUrl }, + }, + ); + + if (error) { + console.error("[fetchArtistPlaylist] Edge function error:", error); + + // Handle FunctionsHttpError to get the actual error details + if (!(error instanceof FunctionsHttpError)) { + throw error; + } + + let soundcloudError: SoundCloudError | null = null; + try { + const errorDetails = await error.context.json(); + console.error( + "[fetchArtistPlaylist] Function returned error:", + errorDetails, + ); + + // Use the specific error message and code from our edge function + const message = + errorDetails.error || "Failed to fetch SoundCloud playlist"; + const errorCode = errorDetails.code || "UNKNOWN_ERROR"; + + soundcloudError = new SoundCloudError(message, errorCode); + } catch (parseError) { + console.error( + "[fetchArtistPlaylist] Failed to parse error response:", + parseError, + ); + throw new Error("Failed to fetch SoundCloud playlist"); + } + + if (soundcloudError) { + throw soundcloudError; + } + } + + if (!data?.playlist) { + console.error("[fetchArtistPlaylist] No playlist in response:", data); + throw new Error("No playlist found in response"); + } + + console.log("[fetchArtistPlaylist] Received playlist:", data.playlist.title); + return data.playlist; +} + +export function useArtistSoundcloudPlaylist({ + soundcloudUrl, + enabled = true, +}: UseArtistSoundcloudPlaylistOptions) { + return useQuery({ + queryKey: ["soundcloud-playlist", soundcloudUrl], + queryFn: () => fetchArtistPlaylist(soundcloudUrl), + enabled: enabled && Boolean(soundcloudUrl), + retry(failureCount, error) { + if (isSoundCloudError(error)) { + return false; + } + return failureCount < 2; + }, + }); +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index a25007b..0db867d 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -159,10 +159,8 @@ export type Database = { estimated_date: string | null; id: string; image_url: string | null; - last_soundcloud_sync: string | null; name: string; slug: string; - soundcloud_followers: number | null; soundcloud_url: string | null; spotify_url: string | null; stage: string | null; @@ -178,10 +176,8 @@ export type Database = { estimated_date?: string | null; id?: string; image_url?: string | null; - last_soundcloud_sync?: string | null; name: string; slug: string; - soundcloud_followers?: number | null; soundcloud_url?: string | null; spotify_url?: string | null; stage?: string | null; @@ -197,10 +193,8 @@ export type Database = { estimated_date?: string | null; id?: string; image_url?: string | null; - last_soundcloud_sync?: string | null; name?: string; slug?: string; - soundcloud_followers?: number | null; soundcloud_url?: string | null; spotify_url?: string | null; stage?: string | null; @@ -637,6 +631,59 @@ export type Database = { }, ]; }; + soundcloud: { + Row: { + artist_id: string; + created_at: string | null; + display_name: string | null; + followers_count: number | null; + id: string; + last_sync: string | null; + playlist_title: string | null; + playlist_url: string | null; + soundcloud_id: number | null; + soundcloud_url: string; + updated_at: string | null; + username: string | null; + }; + Insert: { + artist_id: string; + created_at?: string | null; + display_name?: string | null; + followers_count?: number | null; + id?: string; + last_sync?: string | null; + playlist_title?: string | null; + playlist_url?: string | null; + soundcloud_id?: number | null; + soundcloud_url: string; + updated_at?: string | null; + username?: string | null; + }; + Update: { + artist_id?: string; + created_at?: string | null; + display_name?: string | null; + followers_count?: number | null; + id?: string; + last_sync?: string | null; + playlist_title?: string | null; + playlist_url?: string | null; + soundcloud_id?: number | null; + soundcloud_url?: string; + updated_at?: string | null; + username?: string | null; + }; + Relationships: [ + { + foreignKeyName: "soundcloud_artist_id_fkey"; + columns: ["artist_id"]; + isOneToOne: true; + referencedRelation: "artists"; + referencedColumns: ["id"]; + }, + ]; + }; stages: { Row: { archived: boolean; diff --git a/src/pages/EditionView/TabNavigation/config.ts b/src/pages/EditionView/TabNavigation/config.ts index 05139cb..c50754b 100644 --- a/src/pages/EditionView/TabNavigation/config.ts +++ b/src/pages/EditionView/TabNavigation/config.ts @@ -1,5 +1,6 @@ import { CalendarIcon, + HeartIcon, InfoIcon, ListIcon, MapIcon, @@ -22,6 +23,13 @@ export const config: TabConfig[] = [ shortLabel: "Schedule", enabled: true, }, + { + icon: HeartIcon, + label: "Explore", + shortLabel: "Explore", + enabled: true, + key: "explore", + }, { key: "map", icon: MapIcon, diff --git a/src/pages/EditionView/TabNavigation/types.ts b/src/pages/EditionView/TabNavigation/types.ts index 4deed3d..42125ac 100644 --- a/src/pages/EditionView/TabNavigation/types.ts +++ b/src/pages/EditionView/TabNavigation/types.ts @@ -1,7 +1,13 @@ import { FestivalInfo } from "@/hooks/queries/festival-info/useFestivalInfo"; import { LucideIcon } from "lucide-react"; -export type MainTab = "sets" | "schedule" | "map" | "info" | "social"; +export type MainTab = + | "sets" + | "schedule" + | "map" + | "info" + | "social" + | "explore"; export type TabConfig = { key: MainTab; diff --git a/src/pages/ExploreSetPage/ExplorationProgress.tsx b/src/pages/ExploreSetPage/ExplorationProgress.tsx new file mode 100644 index 0000000..a2f6c3c --- /dev/null +++ b/src/pages/ExploreSetPage/ExplorationProgress.tsx @@ -0,0 +1,24 @@ +import { Progress } from "@/components/ui/progress"; + +interface ExplorationProgressProps { + current: number; + total: number; +} + +export function ExplorationProgress({ + current, + total, +}: ExplorationProgressProps) { + const percentage = (current / total) * 100; + + return ( +
+ + {current} of {total} + +
+ +
+
+ ); +} diff --git a/src/pages/ExploreSetPage/ExploreSetPage.tsx b/src/pages/ExploreSetPage/ExploreSetPage.tsx new file mode 100644 index 0000000..05512ea --- /dev/null +++ b/src/pages/ExploreSetPage/ExploreSetPage.tsx @@ -0,0 +1,150 @@ +import { useNavigate } from "react-router-dom"; +import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { LoadingState } from "./components/LoadingState"; +import { EmptyState } from "./components/EmptyState"; +import { ExplorePageHeader } from "./components/ExplorePageHeader"; +import { CardStackContainer } from "./components/CardStackContainer"; +import { VotingSection } from "./components/VotingSection"; +import { useAuth } from "@/contexts/AuthContext"; +import { useVote } from "@/hooks/queries/voting/useVote"; +import { useUserVotes } from "@/hooks/queries/voting/useUserVotes"; +import { useSetsByEditionQuery } from "@/hooks/queries/sets/useSetsByEdition"; +import { useMemo, useState } from "react"; + +export function ExploreSetPage() { + const { edition, basePath } = useFestivalEdition(); + const navigate = useNavigate(); + const { user, showAuthDialog } = useAuth(); + const voteMutation = useVote(); + const { data: userVotes = {} } = useUserVotes(user?.id || ""); + + // Fetch edition and sets data + const { data: allSets = [], isLoading: setsLoading } = useSetsByEditionQuery( + edition?.id, + ); + + // Filter to sets with artists and valid data + const explorableSets = useMemo(() => { + return allSets.filter( + (set) => + set.artists && + set.artists.length > 0 && + set.name && + set.artists[0].soundcloud_url, + ); + }, [allSets]); + + const [currentIndex, setCurrentIndex] = useState(0); + const [direction, setDirection] = useState<"left" | "right" | null>(null); + const [dragFeedback, setDragFeedback] = useState<{ + direction: "left" | "right" | null; + intensity: number; + }>({ direction: null, intensity: 0 }); + + const currentSet = explorableSets[currentIndex]; + const isLastSet = currentIndex >= explorableSets.length - 1; + + async function handleVote(voteType: number) { + if (!currentSet) return; + + if (!user) { + showAuthDialog(); + return; + } + + const existingVote = userVotes[currentSet.id]; + + try { + await voteMutation.mutateAsync({ + setId: currentSet.id, + voteType, + userId: user.id, + existingVote, + }); + + // Set direction based on vote type for animation + setDirection(voteType >= 1 ? "right" : "left"); + + // Move to next set after animation + setTimeout(() => { + if (isLastSet) { + // Navigate back or show completion screen + navigate(`${basePath}/sets`); + } else { + setCurrentIndex((prev) => prev + 1); + setDirection(null); + } + }, 300); + } catch (error) { + console.error("Failed to vote:", error); + } + } + + function handleSwipe(direction: "left" | "right") { + if (direction === "left") { + handleVote(-1); // Won't Go + } else { + handleVote(1); // Interested + } + } + + function handleDragUpdate( + direction: "left" | "right" | null, + intensity: number, + ) { + setDragFeedback({ direction, intensity }); + } + + function handleSkip() { + setDirection("left"); + setTimeout(() => { + if (isLastSet) { + navigate(-1); + } else { + setCurrentIndex((prev) => prev + 1); + setDirection(null); + } + }, 300); + } + + if (setsLoading) { + return ; + } + + const totalSets = explorableSets.length; + const nextSet = !isLastSet ? explorableSets[currentIndex + 1] : undefined; + + if (!edition || totalSets === 0) { + return navigate(-1)} />; + } + + return ( +
+ {/* Header */} + + + {/* Card Stack */} + + + {/* Voting Actions */} + +
+ ); +} diff --git a/src/pages/ExploreSetPage/SetExploreCard.tsx b/src/pages/ExploreSetPage/SetExploreCard.tsx new file mode 100644 index 0000000..6e2c95f --- /dev/null +++ b/src/pages/ExploreSetPage/SetExploreCard.tsx @@ -0,0 +1,152 @@ +import { FestivalSet } from "@/hooks/queries/sets/useSets"; +import { Card } from "@/components/ui/card"; +import { motion, PanInfo } from "framer-motion"; +import { useState } from "react"; +import { SetCardHeader } from "./SetExploreCard/SetCardHeader"; +import { PrimaryArtistDisplay } from "./SetExploreCard/PrimaryArtistDisplay"; +import { SupportingArtists } from "./SetExploreCard/SupportingArtists"; +import { SetAudioPlayer } from "./SetExploreCard/SetAudioPlayer"; + +interface SetExploreCardProps { + set: FestivalSet; + isFront?: boolean; + onSwipe?: (direction: "left" | "right") => void; + onTap?: () => void; + onDragUpdate?: ( + direction: "left" | "right" | null, + intensity: number, + ) => void; +} + +export function SetExploreCard({ + set, + isFront, + onSwipe, + onTap, + onDragUpdate, +}: SetExploreCardProps) { + const [imageLoaded, setImageLoaded] = useState(false); + const [isDragging, setIsDragging] = useState(false); + + // Get primary artist (first artist) for main display + const primaryArtist = set.artists[0]; + const supportingArtists = set.artists.slice(1); + + return ( + + + {/* Background Image */} +
+ {primaryArtist?.image_url && ( + <> + {primaryArtist.name} setImageLoaded(true)} + /> +
+ + )} + + {/* Content */} +
+ {/* Header Info */} + + + {/* Main Content */} +
+ {/* Set Name */} +
+

{set.name}

+
+ + {/* Primary Artist */} + {primaryArtist && ( + e.stopPropagation()} + /> + )} + + {/* Supporting Artists */} + +
+ +
+ +
+ + {/* Footer */} +
+

+ Swipe or tap buttons to vote +

+
+
+
+ + + ); + + function handleDragEnd( + _event: MouseEvent | TouchEvent | PointerEvent, + info: PanInfo, + ) { + // Reset drag feedback + onDragUpdate?.(null, 0); + setIsDragging(false); + + const swipeThreshold = 100; + const velocityThreshold = 500; + + if ( + Math.abs(info.offset.x) > swipeThreshold || + Math.abs(info.velocity.x) > velocityThreshold + ) { + if (info.offset.x > 0) { + onSwipe?.("right"); + } else { + onSwipe?.("left"); + } + } + } + + function handleDrag( + _event: MouseEvent | TouchEvent | PointerEvent, + info: PanInfo, + ) { + const dragDistance = Math.abs(info.offset.x); + const maxDistance = 150; // Max distance for full intensity + const intensity = Math.min(dragDistance / maxDistance, 1); + + if (dragDistance > 10) { + // Minimum drag threshold + const direction = info.offset.x > 0 ? "right" : "left"; + setIsDragging(true); + onDragUpdate?.(direction, intensity); + } else { + setIsDragging(false); + onDragUpdate?.(null, 0); + } + } +} diff --git a/src/pages/ExploreSetPage/SetExploreCard/PrimaryArtistDisplay.tsx b/src/pages/ExploreSetPage/SetExploreCard/PrimaryArtistDisplay.tsx new file mode 100644 index 0000000..3958a95 --- /dev/null +++ b/src/pages/ExploreSetPage/SetExploreCard/PrimaryArtistDisplay.tsx @@ -0,0 +1,44 @@ +import { Users } from "lucide-react"; +import { SoundCloudBadge } from "./SoundCloudBadge"; +import { Artist } from "@/hooks/queries/artists/useArtists"; + +interface PrimaryArtistDisplayProps { + artist: Artist; + onSoundCloudClick?: (e: React.MouseEvent) => void; +} + +export function PrimaryArtistDisplay({ + artist, + onSoundCloudClick, +}: PrimaryArtistDisplayProps) { + return ( +
+
+ {artist.image_url ? ( + {artist.name} + ) : ( +
+ +
+ )} +
+

{artist.name}

+ {artist.description && ( +

+ {artist.description} +

+ )} + +
+ +
+
+ ); +} diff --git a/src/pages/ExploreSetPage/SetExploreCard/SetAudioPlayer.tsx b/src/pages/ExploreSetPage/SetExploreCard/SetAudioPlayer.tsx new file mode 100644 index 0000000..33891ae --- /dev/null +++ b/src/pages/ExploreSetPage/SetExploreCard/SetAudioPlayer.tsx @@ -0,0 +1,44 @@ +interface SetAudioPlayerProps { + soundcloudUrl?: string; + isActive?: boolean; +} + +export function SetAudioPlayer({ + soundcloudUrl, + isActive = true, +}: SetAudioPlayerProps) { + if (!isActive || !soundcloudUrl) { + return null; + } + + // Build SoundCloud iframe URL with parameters + const baseUrl = "https://w.soundcloud.com/player/"; + const encodedUrl = encodeURIComponent(soundcloudUrl); + const params = new URLSearchParams({ + url: encodedUrl, + auto_play: "true", + color: "8b5cf6", + buying: "false", + sharing: "false", + show_artwork: "false", + show_playcount: "false", + show_user: "false", + single_active: "true", + download: "false", + // start_track: "0", + }); + const widgetUrl = `${baseUrl}?${params.toString()}`; + + return ( +
+ {/* SoundCloud iframe player */} +