├── src ├── services │ ├── config.ts │ ├── supabaseClient.ts │ ├── authService.ts │ └── super-complex-ai-from-scratch.ts ├── typings │ ├── motion.d.ts │ └── Snack.ts ├── vite-env.d.ts ├── assets │ ├── fonts │ │ ├── Poppins-Bold.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Poppins-Regular.ttf │ │ └── RobotoMono-VariableFont_wght.ttf │ ├── icons │ │ ├── upvote-solid.svg │ │ ├── downvote-solid.svg │ │ ├── upvote-empty.svg │ │ └── downvote-empty.svg │ └── donut.svg ├── index.tsx ├── pages │ ├── SnackAi.tsx │ ├── Request.tsx │ ├── Votes.tsx │ ├── SnackStats.tsx │ ├── NotFound.tsx │ ├── Home.tsx │ ├── Profile.tsx │ ├── LogIn.tsx │ └── About.tsx ├── styles │ ├── colors.scss │ ├── typography.scss │ └── globals.scss ├── components │ ├── atoms │ │ ├── Card.tsx │ │ └── Button.tsx │ ├── Furniture │ │ ├── Loading.tsx │ │ ├── Footer.tsx │ │ └── Navbar │ │ │ ├── Navbar.tsx │ │ │ └── Navbar.scss │ ├── AccountFragments │ │ ├── AccountDeletion.tsx │ │ ├── AccountInfo.tsx │ │ ├── AccountData.tsx │ │ ├── AccountPreferences.tsx │ │ └── AccountAllergens.tsx │ ├── other │ │ └── PromptForPreferences.tsx │ └── SnackStuff │ │ ├── SnackList.tsx │ │ ├── SnackCountdown.tsx │ │ ├── RequestSnack.tsx │ │ ├── SnackSuggestions.tsx │ │ └── SnackVoting.tsx └── Routes.tsx ├── public ├── _redirects ├── banner.png ├── favicon.ico ├── pwa │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png ├── snack-champ-bot.png ├── security.txt ├── terms.txt ├── manifest.json ├── snack_champion.svg └── privacy.txt ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── package.json ├── tsconfig.json ├── LICENSE ├── index.html ├── README.md └── yarn.lock /src/services/config.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/typings/motion.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@motionone/solid'; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/pwa/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/pwa/favicon.ico -------------------------------------------------------------------------------- /public/pwa/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/pwa/favicon-16x16.png -------------------------------------------------------------------------------- /public/pwa/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/pwa/favicon-32x32.png -------------------------------------------------------------------------------- /public/snack-champ-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/snack-champ-bot.png -------------------------------------------------------------------------------- /public/pwa/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/pwa/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/fonts/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/src/assets/fonts/Poppins-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/src/assets/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/src/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/pwa/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/pwa/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/pwa/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/public/pwa/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/fonts/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/src/assets/fonts/Poppins-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/cso/HEAD/src/assets/fonts/RobotoMono-VariableFont_wght.ttf -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import solid from 'vite-plugin-solid' 3 | 4 | export default defineConfig({ 5 | plugins: [solid()], 6 | }) 7 | -------------------------------------------------------------------------------- /public/security.txt: -------------------------------------------------------------------------------- 1 | Contact: mailto:security@mail.alicia.omg.lol 2 | Expires: 2024-12-31T00:00:00.000Z 3 | Encryption: https://keybase.io/aliciasykes/pgp_keys.asc?fingerprint=0688f8d34587d954e9e51fb8fedb68f55c0283a7 4 | Preferred-Languages: en 5 | Canonical: /security.txt 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/services/supabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; 4 | const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY; 5 | const supabase = createClient(supabaseUrl, supabaseKey) 6 | 7 | export default supabase; 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'solid-js/web'; 2 | import AppRoutes from './Routes'; 3 | 4 | import './styles/globals.scss'; 5 | import './styles/typography.scss'; 6 | import './styles/colors.scss'; 7 | 8 | render( 9 | () => (), 10 | document.getElementById('snack-champion-root')! 11 | ); 12 | -------------------------------------------------------------------------------- /src/pages/SnackAi.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'solid-styled-components'; 2 | import SnackSuggestions from '../components/SnackStuff/SnackSuggestions'; 3 | 4 | const SnackAIWrapper = styled('div')` 5 | max-width: 1200px; 6 | margin: 0 auto; 7 | padding: 1rem; 8 | `; 9 | 10 | export default function SnackAi() { 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # Dependencies 11 | node_modules 12 | 13 | # Build files 14 | dist 15 | dist-ssr 16 | *.local 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | # Secrets 30 | .env 31 | .env.* 32 | -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --primary: #d82036; 4 | --secondary: #61dafb; 5 | --tertiary: #646cff; 6 | --quaternary: #888; 7 | --quinary: #fff; 8 | --senary: #000; 9 | --primary-opposite: #0da2d5; 10 | 11 | --primary-darker: #b71c2b; 12 | --foreground: var(--quinary); 13 | --background: #161719; 14 | --background-lighter: #2d2e30; 15 | --background-lighter-lighter: #777; 16 | 17 | // Action Colors 18 | --info: #35c9fa; 19 | --success: #88ff88; 20 | --warning: #ece715; 21 | --danger: #f80363; 22 | } 23 | -------------------------------------------------------------------------------- /src/typings/Snack.ts: -------------------------------------------------------------------------------- 1 | 2 | export type SnackCategory = 'sweet' | 'savory' | 'healthy' | 'drink'; 3 | 4 | export interface Snack { 5 | snack_id: string; 6 | snack_name: string; 7 | user_id: string; 8 | created_at: string; 9 | snack_category?: SnackCategory; 10 | snack_meta: string; 11 | } 12 | 13 | export type SnackVote = 'up' | 'down'; 14 | 15 | export interface Vote { 16 | vote_id: string; 17 | vote: SnackVote; 18 | snack_id: string; 19 | user_id: string; 20 | created_at: string; 21 | } 22 | 23 | export interface SnackWithVotes extends Snack { 24 | Votes: Vote[]; 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/Request.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'solid-styled-components'; 2 | 3 | import RequestSnack from '../components/SnackStuff/RequestSnack'; 4 | 5 | const RequestWrapper = styled('div')` 6 | max-width: 1200px; 7 | margin: 0 auto; 8 | padding: 1rem; 9 | flex: 1; 10 | `; 11 | 12 | const Content = styled('div')` 13 | max-width: 1000px; 14 | width: 80vw; 15 | margin: 0 auto; 16 | `; 17 | 18 | export default function HomePage() { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/icons/upvote-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/downvote-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/atoms/Card.tsx: -------------------------------------------------------------------------------- 1 | import { Component, JSX } from 'solid-js'; 2 | import { styled } from 'solid-styled-components'; 3 | 4 | interface CardProps { 5 | children: JSX.Element; 6 | style?: string; 7 | } 8 | 9 | const StyledCard = styled('div')` 10 | background: var(--background-lighter); 11 | border-radius: 8px; 12 | overflow: hidden; 13 | padding: 0.5rem 1rem; 14 | box-shadow: 2px 2px 3px #0000005e; 15 | `; 16 | 17 | const Card: Component = (props) => { 18 | return ( 19 | 20 |
21 | {props.children} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Card; 28 | -------------------------------------------------------------------------------- /src/assets/icons/upvote-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snack-champion", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@formkit/auto-animate": "^0.8.1", 13 | "@motionone/solid": "^10.16.4", 14 | "@solidjs/router": "^0.9.1", 15 | "@suid/material": "^0.15.1", 16 | "@supabase/supabase-js": "^2.38.5", 17 | "solid-js": "^1.8.5", 18 | "solid-styled-components": "^0.28.5", 19 | "solid-toast": "^0.5.0" 20 | }, 21 | "devDependencies": { 22 | "sass": "^1.69.5", 23 | "typescript": "^5.2.2", 24 | "vite": "^5.0.0", 25 | "vite-plugin-solid": "^2.7.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/icons/downvote-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | "jsxImportSource": "solid-js", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/Votes.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'solid-styled-components'; 2 | import SnackVoting from '../components/SnackStuff/SnackVoting'; 3 | 4 | const HomeWrapper = styled('div')` 5 | max-width: 1200px; 6 | margin: 0 auto; 7 | padding: 1rem; 8 | flex: 1; 9 | `; 10 | 11 | const Content = styled('div')` 12 | max-width: 1000px; 13 | width: 80vw; 14 | margin: 0 auto; 15 | display: flex; 16 | flex-direction: column; 17 | gap: 2rem; 18 | `; 19 | 20 | export default function HomePage() { 21 | return ( 22 | 23 | 24 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /public/terms.txt: -------------------------------------------------------------------------------- 1 | TERMS OF SERVICE 2 | ---------------- 3 | 4 | Effective Date: 1st December 2023 5 | 6 | Welcome to Snack Champion! By using our app, you agree to the following terms: 7 | 8 | 1. Acceptable Use 9 | ----------------- 10 | Be Sensible: Users are expected to act sensibly and respectfully at all times. 11 | No Disruption: Do not disrupt the service or impair the experience of other users. 12 | Avoid Malicious Activities: Engaging in malicious activities or behaviors is strictly prohibited. 13 | 14 | 2. Consequences of Violation 15 | ---------------------------- 16 | Violation of these terms may result in restriction of access or permanent ban from the app. 17 | 18 | 3. Changes to Terms 19 | ------------------- 20 | We may update these terms from time to time. Continued use of the app after any such changes indicates your acceptance of the new terms. 21 | -------------------------------------------------------------------------------- /src/pages/SnackStats.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'solid-styled-components'; 2 | 3 | import Card from '../components/atoms/Card'; 4 | import { Alert, AlertTitle } from '@suid/material'; 5 | 6 | const SnackStatsWrapper = styled('div')` 7 | max-width: 1200px; 8 | margin: 0 auto; 9 | padding: 1rem; 10 | `; 11 | 12 | const Subheading = styled('h2')` 13 | margin: 0.5rem 0; 14 | font-size: 2rem; 15 | `; 16 | 17 | export default function SnackStats() { 18 | return ( 19 | 20 | 21 | Snack Stats 22 | 23 | Coming Soon 24 | There's not currently enough vote data to show stats.
25 | Check back soon for fun graphs and charts! 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Snack Champion", 3 | "short_name": "SnackChamp", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "background_color": "#161719", 7 | "theme_color": "#d82036", 8 | "description": "A platform for managing office snacks efficiently.", 9 | "icons": [ 10 | { 11 | "src": "/pwa/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/pwa/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/pwa/apple-touch-icon.png", 22 | "sizes": "180x180", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "/pwa/favicon-32x32.png", 27 | "sizes": "32x32", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "/pwa/favicon-16x16.png", 32 | "sizes": "16x16", 33 | "type": "image/png" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'solid-styled-components'; 2 | import toast from 'solid-toast'; 3 | import Button from '../components/atoms/Button'; 4 | 5 | const NotFoundWrapper = styled('div')` 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | 10 | h1 { 11 | font-size: 20vw; 12 | text-align: center; 13 | text-shadow: 20px 20px 20px #000000b2, -4px -4px 4px black; 14 | opacity: 0.15; 15 | margin: 1rem auto; 16 | } 17 | .why-no-find-page { 18 | color: var(--primary); 19 | font-size: 3rem; 20 | text-align: center; 21 | margin: 0.5rem auto 2rem auto; 22 | } 23 | ` 24 | 25 | export default function NotFoundPage() { 26 | 27 | const goHome = () => { 28 | toast('Sorry about that! We\'re taking you back home now...'); 29 | window.location.href = '/'; 30 | }; 31 | 32 | return ( 33 | 34 |

404

35 |

Page not Found 😢

36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/typography.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | @font-face { 4 | font-family: 'Poppins'; 5 | src: url('../assets/fonts/Poppins-Regular.ttf') format('truetype'); 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: 'PoppinsBold'; 12 | src: url('../assets/fonts/Poppins-Bold.ttf') format('truetype'); 13 | font-weight: bold; 14 | font-style: normal; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Roboto'; 19 | src: url('../assets/fonts/Roboto-Regular.ttf') format('truetype'); 20 | font-weight: normal; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: 'RobotoThin'; 26 | src: url('../assets/fonts/Roboto-Thin.ttf') format('truetype'); 27 | font-weight: 100; 28 | font-style: normal; 29 | } 30 | 31 | @font-face { 32 | font-family: 'RobotoMono'; 33 | src: url('../assets/fonts/RobotoMono-VariableFont_wght.ttf') format('truetype'); 34 | font-weight: normal; 35 | font-style: normal; 36 | } 37 | 38 | body { 39 | font-family: 'Poppins', sans-serif; 40 | } 41 | 42 | h1, h2, h3, h4, h5, h6 { 43 | font-family: 'PoppinsBold', sans-serif; 44 | } 45 | -------------------------------------------------------------------------------- /public/snack_champion.svg: -------------------------------------------------------------------------------- 1 | 2 | Snack Champion 3 | 4 | 5 | 6 | 7 | 8 | 9 | Layer 1 10 | 11 | 12 | SC 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alicia Sykes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/atoms/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Component, JSX } from 'solid-js'; 2 | 3 | interface ButtonProps { 4 | children: JSX.Element; 5 | onClick: () => void; 6 | size?: 'small' | 'medium' | 'large'; 7 | } 8 | 9 | const styles = ` 10 | .button { 11 | background: var(--primary); 12 | color: var(--foreground); 13 | border-radius: 4px; 14 | outline: none; 15 | border: none; 16 | font-family: PoppinsBold; 17 | cursor: pointer; 18 | transition: background 0.2s ease-in-out; 19 | } 20 | .button:hover { 21 | background: var(--primary-darker); 22 | } 23 | `; 24 | 25 | const sizes = { 26 | small: `.button { 27 | font-size: 0.75rem; 28 | }`, 29 | medium: `.button { 30 | font-size: 1.5rem; 31 | padding: 0.25rem 0.5rem; 32 | height: 100%; 33 | }`, 34 | large: `.button { 35 | font-size: 2rem; 36 | margin: 1rem auto; 37 | padding: 0.75rem 2rem; 38 | }`, 39 | }; 40 | 41 | const Button: Component = (props) => { 42 | const size = props.size || 'medium'; 43 | 44 | return ( 45 | <> 46 | 47 | 50 | 51 | ); 52 | }; 53 | 54 | export default Button; 55 | -------------------------------------------------------------------------------- /src/components/Furniture/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createSignal, onCleanup } from 'solid-js'; 2 | import { styled } from 'solid-styled-components'; 3 | import { CircularProgress } from '@suid/material' 4 | 5 | const LoadWrap = styled('div')` 6 | 7 | margin: 5rem auto; 8 | padding: 1rem; 9 | text-align: center; 10 | h2 { 11 | font-size: 4rem; 12 | margin: 0.5rem 0; 13 | &.meow { font-size: 8rem; } 14 | } 15 | p { 16 | font-size: 1rem; 17 | margin: 2rem auto; 18 | font-family: RobotoMono, sans-serif; 19 | } 20 | `; 21 | 22 | const Loading: Component = () => { 23 | 24 | const [isProbzError, setIsProbzError] = createSignal(false); 25 | 26 | const timer = setTimeout(() => setIsProbzError(true), 3500); 27 | 28 | onCleanup(() => clearTimeout(timer)); 29 | 30 | return ( 31 | 32 | { isProbzError()? ( 33 | <> 34 |

Oops

35 |

😿

36 |

37 | It looks like something unexpected has happened 38 |

39 |

40 | Try refreshing the page, or logging in/out again.
41 | If the issue persists, please report it so we can get it fixed. 42 |

43 | 44 | ) : ( 45 | <> 46 |

Loading

47 | 48 |

Just a sec...

49 | 50 | ) 51 | } 52 |
53 | ); 54 | }; 55 | 56 | export default Loading; 57 | -------------------------------------------------------------------------------- /src/components/Furniture/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { styled } from 'solid-styled-components'; 3 | 4 | const StyledFooter = styled('footer')` 5 | background: var(--background-lighter); 6 | text-align: center; 7 | padding: 0.2rem 0; 8 | opacity: 0.8; 9 | a { 10 | color: var(--primary); 11 | font-weight: bold; 12 | text-decoration: none; 13 | margin: 0 2px; 14 | padding: 0 2px; 15 | border-radius: 3px; 16 | transition: all 0.2s ease-in-out; 17 | &:hover { 18 | color: var(--foreground); 19 | background: var(--primary-darker); 20 | } 21 | } 22 | `; 23 | 24 | 25 | const Footer: Component = () => { 26 | 27 | const footerContent = { 28 | appName: 'Snack Champion', 29 | appUrl: '/about', 30 | githubUrl: 'https://github.com/lissy93/cso', 31 | developer: 'Alicia Sykes', 32 | developerUrl: 'https://aliciasykes.com', 33 | license: 'MIT', 34 | licenseUrl: 'https://github.com/lissy93/cso/blob/main/LICENSE', 35 | licenseDate: new Date().getFullYear(), 36 | }; 37 | 38 | return ( 39 | 40 | {footerContent.appName} is 41 | licensed under {footerContent.license} © 42 | {footerContent.developer} {footerContent.licenseDate} | 43 | Source code available on GitHub. 44 | 45 | ); 46 | }; 47 | 48 | export default Footer; 49 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'solid-styled-components'; 2 | 3 | import RequestSnack from '../components/SnackStuff/RequestSnack'; 4 | import SnackVoting from '../components/SnackStuff/SnackVoting'; 5 | import SnackSuggestions from '../components/SnackStuff/SnackSuggestions'; 6 | import SnackCountdown from '../components/SnackStuff/SnackCountdown'; 7 | import PromptForPreferences from '../components/other/PromptForPreferences'; 8 | 9 | const HomeWrapper = styled('div')` 10 | padding-bottom: 2rem; 11 | flex: 1; 12 | `; 13 | 14 | const Content = styled('div')` 15 | max-width: 1200px; 16 | width: 80vw; 17 | margin: 0 auto; 18 | 19 | display: grid; 20 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 21 | gap: 2rem; 22 | 23 | margin: auto; 24 | 25 | `; 26 | 27 | export default function HomePage() { 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/donut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/services/authService.ts: -------------------------------------------------------------------------------- 1 | import supabase from './supabaseClient'; 2 | import { createSignal, createEffect } from 'solid-js'; 3 | import toast from 'solid-toast'; 4 | 5 | // Signals for authentication state and user details 6 | export const [isAuthenticated, setIsAuthenticated] = createSignal(false); 7 | const [userEmail, setUserEmail] = createSignal(''); 8 | 9 | // Check and update authentication state 10 | createEffect(async () => { 11 | const {data: { session },} = await supabase.auth.getSession() 12 | setIsAuthenticated(!!session); 13 | setUserEmail(session?.user?.email || ''); 14 | localStorage.setItem('supabase.auth.token', session?.access_token || ''); 15 | }); 16 | 17 | export const login = async () => { 18 | const { error } = await supabase.auth.signInWithOAuth({ 19 | provider: "google", 20 | options: { 21 | queryParams: { 22 | access_type: "offline", 23 | prompt: "consent", 24 | }, 25 | }, 26 | }); 27 | if (error) { 28 | toast.error(`We were unable to authenticate you\n${error.message}`); 29 | } 30 | } 31 | 32 | export const logout = async () => { 33 | await supabase.auth.signOut(); 34 | localStorage.removeItem('supabase.auth.token'); 35 | setIsAuthenticated(false); 36 | setUserEmail(''); 37 | } 38 | 39 | export const rehydrateAuth = () => { 40 | supabase.auth.getSession() 41 | }; 42 | 43 | export const useUserEmail = () => userEmail; 44 | 45 | export const fetchUserFromSession = async () => { 46 | const session = await (await supabase.auth.getSession()).data.session 47 | return session?.user 48 | }; 49 | -------------------------------------------------------------------------------- /src/pages/Profile.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { styled } from "solid-styled-components"; 3 | 4 | import Card from '../components/atoms/Card'; 5 | 6 | import AccountPreferences from '../components/AccountFragments/AccountPreferences'; 7 | import AccountInfo from '../components/AccountFragments/AccountInfo'; 8 | import AccountDeletion from '../components/AccountFragments/AccountDeletion'; 9 | import AccountData from '../components/AccountFragments/AccountData'; 10 | import AccountAllergens from '../components/AccountFragments/AccountAllergens'; 11 | 12 | const ProfileWrapper = styled('div')` 13 | display: grid; 14 | grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); 15 | gap: 2rem; 16 | padding: 2rem 0; 17 | margin: 0 auto; 18 | width: 80vw; 19 | max-width: 1200px; 20 | `; 21 | 22 | const SubHeading = styled('h2')` 23 | margin: 0.25rem 0; 24 | `; 25 | 26 | const AboutText = styled('p')` 27 | margin: 0; 28 | font-family: RobotoMono, monospace; 29 | font-size: 0.9rem; 30 | a { color: var(--primary); } 31 | `; 32 | 33 | export default function Profile () { 34 | 35 | return ( 36 | 37 | <> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Further Info 46 | For privacy policy, ToS, docs and more - please see the About Page. 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Snack Champion 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Poppins, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: var(--foreground, rgba(255, 255, 255, 0.87)); 8 | background-color: var(--background, #161719); 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | ::selection { 17 | background-color: var(--primary); 18 | color: var(--foreground); 19 | } 20 | 21 | body { 22 | margin: 0; 23 | padding: 0; 24 | background-color: var(--background, #161719); 25 | 26 | scrollbar-width: thin; 27 | scrollbar-color: var(--primary) var(--background-lighter); 28 | &::-webkit-scrollbar { 29 | width: 0.5rem; 30 | } 31 | &::-webkit-scrollbar-track { 32 | background: var(--background-lighter); 33 | } 34 | &::-webkit-scrollbar-thumb { 35 | background: var(--primary); 36 | border-radius: 3px; 37 | border-left: 1px solid var(--background-lighter); 38 | } 39 | } 40 | 41 | main { 42 | flex: 1; 43 | } 44 | 45 | #snack-champion-root { 46 | min-width: 100%; 47 | display: flex; 48 | flex-direction: column; 49 | min-height: 100vh; 50 | } 51 | 52 | .logo { 53 | height: 6em; 54 | padding: 1.5em; 55 | will-change: filter; 56 | transition: filter 300ms; 57 | } 58 | .logo:hover { 59 | filter: drop-shadow(0 0 2em #646cffaa); 60 | } 61 | .logo.solid:hover { 62 | filter: drop-shadow(0 0 2em #61dafbaa); 63 | } 64 | 65 | .card { 66 | padding: 2em; 67 | } 68 | 69 | .read-the-docs { 70 | color: #888; 71 | } 72 | 73 | .MuiDialog-paper { 74 | background: var(--background-lighter) !important; 75 | } 76 | 77 | #allergen-select-label, #more-prefs-label, #complete-dietary-preferences { 78 | color: var(--foreground); 79 | } 80 | .MuiAlert-message { width: 100%; } 81 | -------------------------------------------------------------------------------- /src/components/Furniture/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Show, createResource, createSignal } from 'solid-js'; 2 | import { isAuthenticated, fetchUserFromSession, login, logout } from '../../../services/authService'; 3 | import Donut from '../../../assets/donut.svg'; 4 | import './Navbar.scss'; 5 | 6 | const Navbar: Component = () => { 7 | 8 | const [session] = createResource(fetchUserFromSession); 9 | 10 | const [menuOpen, setMenuOpen] = createSignal(false); 11 | 12 | return ( 13 | 52 | ); 53 | }; 54 | 55 | export default Navbar; 56 | -------------------------------------------------------------------------------- /src/pages/LogIn.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from '@solidjs/router'; 2 | 3 | import { styled } from 'solid-styled-components'; 4 | import { isAuthenticated } from '../services/authService'; 5 | import supabase from '../services/supabaseClient'; 6 | import Card from '../components/atoms/Card'; 7 | import Button from '../components/atoms/Button'; 8 | import { createEffect } from 'solid-js'; 9 | 10 | const SnackBot = styled('img')` 11 | position: absolute; 12 | bottom: 0; 13 | right: 1rem; 14 | transition: all 0.2s ease-in-out; 15 | width: 100px; 16 | z-index: 2; 17 | &:hover { 18 | transform: scale(1.1) translateY(-0.5rem) translateX(-0.25rem) rotate(-1deg); 19 | } 20 | `; 21 | 22 | const LoginPage = () => { 23 | 24 | const navigate = useNavigate(); 25 | 26 | // Redirect to home page if already authenticated 27 | createEffect(() => { 28 | if (isAuthenticated()) { 29 | navigate('/'); 30 | } 31 | }); 32 | 33 | const signInWithGoogle = async () => { 34 | const { error } = await supabase.auth.signInWithOAuth({ 35 | provider: 'google', 36 | options: { 37 | queryParams: { 38 | access_type: 'offline', 39 | prompt: 'consent', 40 | }, 41 | }, 42 | }); 43 | if (error) { 44 | alert(error.message); 45 | } 46 | } 47 | 48 | const styles = ` 49 | .intro { 50 | font-family: 'PoppinsBold', sans-serif; 51 | font-size: 2rem; 52 | margin: 0; 53 | } 54 | .line-2 { 55 | font-size: 1.5rem; 56 | } 57 | .what { 58 | margin: 0.5rem 0; 59 | font-style: italic; 60 | font-size: 0.8rem; 61 | opacity: 0.8; 62 | } 63 | .login-wrapper { 64 | max-width: 800px; 65 | margin: 2rem auto; 66 | text-align: center; 67 | flex: 1; 68 | } 69 | `; 70 | 71 | return ( 72 | <> 73 | 74 | 83 | 84 | ); 85 | }; 86 | 87 | export default LoginPage; 88 | -------------------------------------------------------------------------------- /src/services/super-complex-ai-from-scratch.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Just kidding - it's only a prompt that gets sent to OpenAI's API. 4 | */ 5 | 6 | export const prompt = ` 7 | 8 | I am going to provide you with some input data (a list of a snack upvotes and downvotes and any optional preferences), 9 | and I would like you to respond with a comma-separated list of twelve snack suggestions the user may like - without including any extra text. 10 | 11 | The input will be in the following format: 12 | 13 | \`\`\`json 14 | { 15 | "upvoted": [/* A list of snacks the user likes */], 16 | "downvoted": [/* A list of snacks the user dislikes */], 17 | "preferences": [/* Any additional preferences or dietary needs the user may have */] 18 | } 19 | \`\`\` 20 | 21 | Task: 22 | Analyze the input data to understand the user's snack preferences. 23 | Based on the upvoted and downvoted snacks, identify patterns or categories 24 | that the user seems to prefer or avoid. 25 | 26 | Based on the users snack preferences, generate a list of twelve snack suggestions the user may like. 27 | Please ensure that: 28 | - Respond with a comma separated list of twelve snack suggestions. 29 | - Do not add any extra text. 30 | - Do not use any characters other than letters, numbers, commas, and spaces. 31 | - Do not return a response longer than 120 characters. 32 | - Do not respond with any category names, your response should only include snack names. 33 | - DO NOT add any extra text before or after your answer (like "Based on the user's preferences, here are some snack suggestions") - just respond with the snack list itself. 34 | 35 | 36 | The output should be a plain, comma-separated list of twelve snack names. 37 | 38 | 39 | Considerations: 40 | 41 | - Use specific products that are likely available in most British supermarkets. 42 | - Products should be suitable for an office environment. 43 | - Avoid suggesting snacks that are similar to those in the downvoted list. 44 | - Respond with items which are specific products, not generic categories (e.g., "chocolate" is not specific enough, but "Cadbury Dairy Milk" is). 45 | - Only respond with snacks which are suitable for humans to eat. 46 | - Avoid items which are not readily available in most British supermarkets, or that are not suitable for an office setting. 47 | - Ensure the only words that you respond with are snack names - DO NOT include any other words. 48 | 49 | Example Output: Diet Coke, Cadbury Animals, Capri-Sun Zero, Kellogg's Pop Tarts, Soreen Malt Lunchbox Loaves, Mini Babybel Light 50 | `; 51 | 52 | -------------------------------------------------------------------------------- /src/components/AccountFragments/AccountDeletion.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js'; 2 | import { styled } from 'solid-styled-components'; 3 | import Card from '../atoms/Card'; 4 | import Button from '../atoms/Button'; 5 | import { fetchUserFromSession, logout } from '../../services/authService'; 6 | import supabase from '../../services/supabaseClient'; 7 | 8 | import { 9 | Dialog, 10 | DialogActions, 11 | DialogContent, 12 | DialogContentText, 13 | DialogTitle, 14 | Alert, AlertTitle, 15 | } from '@suid/material'; 16 | import toast from 'solid-toast'; 17 | 18 | const SubHeading = styled('h2')` 19 | margin: 0.25rem 0; 20 | `; 21 | 22 | const Info = styled('p')` 23 | margin: 0.25rem 0; 24 | `; 25 | 26 | const Warning = styled('p')` 27 | margin: 0.25rem 0; 28 | font-size: 0.8rem; 29 | color: var(--danger); 30 | b { font-family: 'PoppinsBold', sans-serif; } 31 | `; 32 | 33 | const DialogInner = styled('p')` 34 | margin: 0.25rem 0; 35 | color: var(--foreground); 36 | opacity: 0.9; 37 | `; 38 | 39 | export default function AccountInfo() { 40 | 41 | const [open, setOpen] = createSignal(false); 42 | const handleClickOpen = () => { setOpen(true); }; 43 | const handleClose = () => { setOpen(false); }; 44 | 45 | const deleteAccount = async () => { 46 | const userId = (await fetchUserFromSession())?.id; 47 | if (!userId) return; 48 | const { error } = await supabase.from('Users').delete().eq('id', userId); 49 | if (error) { 50 | toast.error(`Unable to delete account.\n${error.message}`); 51 | return; 52 | } 53 | toast.success('Account deleted successfully.\nGood bye.'); 54 | logout(); 55 | window.location.reload(); 56 | }; 57 | 58 | return ( 59 | 60 | Account Deletion 61 | Delete your account, and all associated data, from Snack Champion 🪦 62 | Warning: This action is irreversible! 63 | 64 | 65 | 66 | Are you sure you'd like to delete your account? 67 | 68 | 69 | 70 | 71 | Important! 72 | This action is permanent and cannot be undone.
73 | All of your data will be deleted from our servers. 74 |
75 | y u no like snak? 76 |
77 |
78 |
79 | 80 | 81 | 82 | 83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Furniture/Navbar/Navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | position: relative; 4 | justify-content: space-between; 5 | background-color: var(--primary); 6 | color: var(--foreground); 7 | width: 100%; 8 | padding: 0; 9 | 10 | .navbar-brand { 11 | font-family: PoppinsBold; 12 | font-size: 1.5rem; 13 | font-weight: bold; 14 | padding: 0.5rem 1rem; 15 | a { 16 | text-decoration: none; 17 | color: var(--foreground); 18 | display: flex; 19 | flex-direction: row; 20 | gap: 1rem; 21 | transition: all 0.2s ease-in-out; 22 | img { 23 | width: 2rem; 24 | } 25 | &:hover { 26 | opacity: 0.8; 27 | transform: scale(1.04); 28 | } 29 | } 30 | } 31 | 32 | .navbar-links { 33 | list-style: none; 34 | display: flex; 35 | margin: 0; 36 | position: relative; 37 | @media (max-width: 600px) { 38 | flex-direction: column; 39 | } 40 | 41 | li { 42 | margin: 0; 43 | display: flex; 44 | align-items: center; 45 | gap: 0.5rem; 46 | padding: 0 0.5rem; 47 | cursor: pointer; 48 | transition: all 0.2s ease-in-out; 49 | img { 50 | border-radius: 50%; 51 | font-size: 1.2rem; 52 | } 53 | &:hover { 54 | background: var(--primary-darker); 55 | } 56 | a, button { 57 | color: var(--foreground); 58 | text-decoration: none; 59 | background: none; 60 | border: none; 61 | cursor: pointer; 62 | font-size: 1rem; 63 | border-radius: 4px; 64 | transition: 0.1s ease-in-out; 65 | height: 100%; 66 | display: flex; 67 | align-items: center; 68 | } 69 | &:not(:last-child) { 70 | border-right: 1px solid var(--primary-darker); 71 | } 72 | } 73 | } 74 | } 75 | 76 | @media (max-width: 600px) { 77 | .navbar { 78 | flex-direction: column; 79 | align-items: flex-start; 80 | } 81 | 82 | .navbar-links { 83 | width: 100%; 84 | padding-left: 0; 85 | 86 | li { 87 | width: 100%; 88 | text-align: left; 89 | } 90 | } 91 | } 92 | 93 | .drop-down { 94 | position: absolute; 95 | top: 100%; 96 | right: 0.5rem; 97 | background: var(--background-lighter); 98 | border-radius: 0 0 4px 4px; 99 | list-style: none; 100 | padding: 0; 101 | z-index: 2; 102 | border: 1px solid var(--background); 103 | box-shadow: 2px 2px 3px #0000005e; 104 | margin: 0; 105 | display: flex; 106 | flex-direction: column; 107 | a, span { 108 | cursor: pointer; 109 | transition: all 0.2s ease-in-out; 110 | text-decoration: none; 111 | color: var(--foreground); 112 | padding: 0.5rem 1rem; 113 | &:hover { 114 | background: var(--primary-darker); 115 | } 116 | &:not(:last-child) { 117 | border-bottom: 1px solid var(--background-lighter-lighter); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /public/privacy.txt: -------------------------------------------------------------------------------- 1 | 2 | PRIVACY POLICY FOR SNACK CHAMPION 3 | --------------------------------- 4 | 5 | Effective Date: 1st December 2023 6 | 7 | Welcome to Snack Champion! Your privacy is important to us. This Privacy Policy explains how we collect, use, protect, and handle your personal information when you use our app. 8 | 9 | 10 | 1. INFORMATION WE COLLECT 11 | ------------------------- 12 | When you log in using your work Google Workspace account or Single Sign-On Provider, we access your full name, email address, and picture. This information is displayed in the app's user interface but is not stored in our databases. 13 | 14 | Data Collection 15 | - Snack Preferences: We store your requested snacks, votes on snacks, and preferences including allergens and dietary requirements. 16 | - Analytics and Error Tracking: We use anonymized analytics (via a self-hosted Plausible instance) and error tracking (via a self-hosted GlitchTip instance) with no personally identifiable information collected. You may opt out of these features at any time. 17 | 18 | 19 | 2. STORAGE OF INFORMATION 20 | ------------------------- 21 | All data is encrypted at rest using libsodium's high-level cryptographic algorithms, implemented via the pgsodium PostgreSQL extension. 22 | 23 | 24 | 3. USE OF INFORMATION 25 | --------------------- 26 | Your information is used to: 27 | - Display your profile within the app. 28 | - Manage your snack requests and preferences. 29 | - Improve app functionality and user experience. 30 | - Conduct anonymized analytics and error tracking to enhance app performance. 31 | 32 | 33 | 4. SHARING OF INFORMATION 34 | ------------------------- 35 | Your requested snacks and the total vote counts are visible to other users, but your individual identity and votes are not disclosed. Your preferences are never shared with other users. 36 | 37 | 38 | 5. YOUR RIGHTS AND CHOICES 39 | -------------------------- 40 | - Data Access and Export: You may view or export your data at any time. 41 | - Account Deletion: You have the right to delete your account and all associated data. 42 | - Opt-Out: You can disable error tracking and analytics. We also respect browser Do Not Track settings and ad-blockers. 43 | 44 | 45 | 6. SECURITY 46 | ----------- 47 | We are committed to protecting your data. Our use of encryption and open-source code allows for transparency and security verification. 48 | 49 | 50 | 7. THIRD-PARTY SERVICES 51 | ----------------------- 52 | We utilize the following services: 53 | - Cloudflare (domain, DNS, caching, DDoS protection) 54 | - Netlify (frontend hosting) 55 | - Supabase (Postgres database hosting) 56 | - Google (OAuth) 57 | - OpenAI (Snack AI feature) 58 | - Tesco.com (product photos) 59 | - GitHub (code hosting and CI/CD) 60 | - Solid.js, TS, SCSS, Vite (frontend development) 61 | 62 | 63 | 8. CONTACT US 64 | ------------- 65 | For any data or privacy concerns, please contact us at snack-champion@mail.alicia.omg.lol. 66 | 67 | 68 | 9. CHANGES TO THIS POLICY 69 | ------------------------- 70 | We may update this policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 71 | 72 | -------------------------------------------------------------------------------- /src/components/other/PromptForPreferences.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Dialog, 4 | DialogContent, 5 | DialogContentText, 6 | } from "@suid/material"; 7 | 8 | import { styled } from "solid-styled-components"; 9 | import Button from "../atoms/Button"; 10 | import { createSignal, onMount } from "solid-js"; 11 | 12 | import toast from "solid-toast"; 13 | 14 | import AccountAllergens from '../AccountFragments/AccountAllergens'; 15 | import supabase from "../../services/supabaseClient"; 16 | import { fetchUserFromSession } from '../../services/authService'; 17 | 18 | const InnerWrapper = styled("div")` 19 | display: flex; 20 | align-items: center; 21 | width: 100%; 22 | justify-content: space-between; 23 | .right { 24 | display: flex; 25 | gap: 1rem; 26 | .dismiss { 27 | cursor: pointer; 28 | } 29 | } 30 | `; 31 | 32 | const PromptForPreferences = () => { 33 | 34 | const [dialogOpen, setDialogOpen] = createSignal(false); 35 | const [isVisible, setIsVisible] = createSignal(false); 36 | 37 | const handleClickOpen = () => { 38 | setDialogOpen(true); 39 | }; 40 | 41 | const handleClose = () => { 42 | setDialogOpen(false); 43 | setIsVisible(false); 44 | }; 45 | 46 | const checkIfUserHasAddedAnythingYet = async () => { 47 | const userId = (await fetchUserFromSession())?.id; 48 | if (!userId) return; 49 | const { data, error } = await supabase 50 | .from('Preferences') 51 | .select('user_id') 52 | .eq('user_id', userId) 53 | .single(); 54 | 55 | setIsVisible((error && error?.code === 'PGRST116') || !data ? true : false); 56 | } 57 | 58 | const dismissPrompt = async () => { 59 | const userId = (await fetchUserFromSession())?.id; 60 | await supabase.from('Preferences').insert([{ user_id: userId }]); 61 | toast.success( 62 | 'Okay, we won\'t show this message again.\n'+ 63 | 'You can update your preferences at anytime in your profile.', 64 | { duration: 5000}, 65 | ); 66 | setIsVisible(false); 67 | }; 68 | 69 | onMount(async () => { 70 | checkIfUserHasAddedAnythingYet(); 71 | }); 72 | 73 | return ( 74 | <> 75 | {isVisible() && ( 76 | 77 | 78 | Looks like you've not yet set your dietary requirements or global snack preferences. 79 |
80 | Dismiss 81 | 82 |
83 |
84 |
) 85 | } 86 | 87 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export default PromptForPreferences; 104 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { JSX, createEffect, createSignal, Show, onMount } from 'solid-js'; 2 | import { ThemeProvider, createTheme } from '@suid/material/styles'; 3 | import { Toaster } from 'solid-toast'; 4 | // import { createAutoAnimateDirective } from '@formkit/auto-animate/solid' 5 | import autoAnimate from '@formkit/auto-animate' 6 | 7 | import { Router, Routes, Route, Navigate } from '@solidjs/router'; 8 | import LoginPage from './pages/LogIn'; 9 | import HomePage from './pages/Home'; 10 | import AboutPage from './pages/About'; 11 | import ProfilePage from './pages/Profile'; 12 | import RequestPage from './pages/Request'; 13 | import VotesPage from './pages/Votes'; 14 | import SnackAiPage from './pages/SnackAi'; 15 | import SnackStatsPage from './pages/SnackStats'; 16 | import NotFoundPage from './pages/NotFound'; 17 | 18 | import Navbar from './components/Furniture/Navbar/Navbar'; 19 | import Footer from './components/Furniture/Footer'; 20 | import Loading from './components/Furniture/Loading'; 21 | import { isAuthenticated } from './services/authService'; 22 | import supabase from './services/supabaseClient'; 23 | 24 | const customTheme = createTheme({ 25 | palette: { 26 | primary: { 27 | main: '#d82036', 28 | }, 29 | secondary: { 30 | main: '#fff', 31 | }, 32 | text: { 33 | primary: '#fff', 34 | secondary: '#b71c2b', 35 | disabled: '#969798', 36 | }, 37 | background: { 38 | paper: '#161719', 39 | }, 40 | mode: 'dark', 41 | }, 42 | }); 43 | 44 | const ProtectedRoute = (props: { component: JSX.Element; }) => { 45 | const [authCheckComplete, setAuthCheckComplete] = createSignal(false); 46 | 47 | createEffect(async () => { 48 | await supabase.auth.getSession(); 49 | setAuthCheckComplete(true); 50 | }); 51 | 52 | return ( 53 | }> 54 | {isAuthenticated() ? props.component : } 55 | 56 | ); 57 | }; 58 | 59 | const AppRoutes = () => { 60 | 61 | let listRef: any; 62 | onMount(() => { 63 | if (listRef) { 64 | autoAnimate(listRef, { duration: 500 }); 65 | } 66 | }); 67 | 68 | return ( 69 | 70 | 71 | 72 |
73 | 74 | } /> 75 | } /> 76 | } />} /> 77 | } />} /> 78 | } />} /> 79 | } />} /> 80 | } />} /> 81 | } />} /> 82 | } /> 83 | 84 |
85 |